diff --git a/.gitattributes b/.gitattributes index 1ff0c423..7b17c9b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,9 @@ ############################################################################### * text=auto +# Linux scripts should have Linux file endings. +*.sh text eol=lf + ############################################################################### # Set default behavior for command prompt diff. # diff --git a/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs new file mode 100644 index 00000000..0fd98f6b --- /dev/null +++ b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs @@ -0,0 +1,84 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// We start up a real(ish) ASP.NET Core web server stack to measure exporter performance end to end. +/// +[MemoryDiagnoser] +public class AspNetCoreExporterBenchmarks +{ + static AspNetCoreExporterBenchmarks() + { + // We use the global singleton metrics registry here, so just populate it with some data. + // Not too much data - we care more about the overhead and do not want to just inflate the numbers. + for (var i = 0; i < 5; i++) + Metrics.CreateGauge("dummy_metric_" + i, "For benchmark purposes", "label1", "label2", "label3").WithLabels("1", "2", "3").Inc(); + } + + [GlobalSetup] + public void Setup() + { + var builder = new WebHostBuilder().UseStartup(); + _server = new TestServer(builder); + _client = _server.CreateClient(); + + // Warmup the ASP.NET Core stack to avoid measuring the ASP.NET Core web server itself in benchmarks. + _client.GetAsync("/metrics").GetAwaiter().GetResult(); + } + + [GlobalCleanup] + public void Cleanup() + { + _client?.Dispose(); + _server?.Dispose(); + } + + private TestServer _server; + private HttpClient _client; + + private sealed class EntryPoint + { +#pragma warning disable CA1822 // Mark members as static + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseHttpMetrics(); + + app.UseEndpoints(endpoints => + { + endpoints.MapMetrics(); + + endpoints.MapGet("ok", context => + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + }); + }); + } +#pragma warning restore CA1822 // Mark members as static + } + + [Benchmark] + public async Task GetMetrics() + { + await _client.GetAsync("/metrics"); + } + + [Benchmark] + public async Task Get200Ok() + { + await _client.GetAsync("/ok"); + } +} diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index dc948687..880d2090 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -1,35 +1,38 @@ - + - - Exe - netcoreapp3.1 - true + + Exe + net8.0 + true - ..\Resources\prometheus-net.snk + ..\Resources\prometheus-net.snk - - true - + enable + disable + True + True + 1591 - - true - + true - - true - + latest + 9999 - - - + True + - - - + + + - - - - + + + + + + + + + diff --git a/Benchmark.NetCore/CounterBenchmarks.cs b/Benchmark.NetCore/CounterBenchmarks.cs new file mode 100644 index 00000000..505ce984 --- /dev/null +++ b/Benchmark.NetCore/CounterBenchmarks.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +public class CounterBenchmarks +{ + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + private readonly Counter _counter; + + public CounterBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + _counter = _factory.CreateCounter("gauge", "help text"); + } + + [Benchmark] + public void IncToCurrentTimeUtc() + { + _counter.IncToCurrentTimeUtc(); + } +} diff --git a/Benchmark.NetCore/ExemplarBenchmarks.cs b/Benchmark.NetCore/ExemplarBenchmarks.cs new file mode 100644 index 00000000..35672fc3 --- /dev/null +++ b/Benchmark.NetCore/ExemplarBenchmarks.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using Prometheus; + +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +//[EventPipeProfiler(EventPipeProfile.CpuSampling)] +public class ExemplarBenchmarks +{ + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + private readonly Counter _counter; + + public ExemplarBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + // We provide the exemplars manually, without using the default behavior. + _factory.ExemplarBehavior = ExemplarBehavior.NoExemplars(); + + _counter = _factory.CreateCounter("gauge", "help text"); + } + + // Just establish a baseline - how much time/memory do we spend if not recording an exemplar. + [Benchmark(Baseline = true)] + public void Observe_NoExemplar() + { + _counter.Inc(123); + } + + // Just as a sanity check, this should not cost us anything extra and may even be cheaper as we skip the default behavior lookup. + [Benchmark] + public void Observe_EmptyExemplar() + { + _counter.Inc(123, Exemplar.None); + } + + private static readonly Exemplar.LabelKey CustomLabelKey1 = Exemplar.Key("my_key"); + private static readonly Exemplar.LabelKey CustomLabelKey2 = Exemplar.Key("my_key2"); + + // A manually specified custom exemplar with some arbitrary value. + [Benchmark] + public void Observe_CustomExemplar() + { + _counter.Inc(123, Exemplar.From(CustomLabelKey1.WithValue("my_value"), CustomLabelKey2.WithValue("my_value2"))); + } + + // An exemplar extracted from the current trace context when there is no trace context. + [Benchmark] + public void Observe_ExemplarFromEmptyTraceContext() + { + _counter.Inc(123, Exemplar.FromTraceContext()); + } + + [GlobalSetup(Targets = new[] { nameof(Observe_ExemplarFromTraceContext) })] + public void Setup_ExemplarFromTraceContext() + { + new Activity("test activity").Start(); + + if (Activity.Current == null) + throw new Exception("Sanity check failed."); + } + + // An exemplar extracted from the current trace context when there is a trace context. + [Benchmark] + public void Observe_ExemplarFromTraceContext() + { + _counter.Inc(123, Exemplar.FromTraceContext()); + } + + [GlobalCleanup(Targets = new[] { nameof(Observe_ExemplarFromEmptyTraceContext), nameof(Observe_ExemplarFromTraceContext) })] + public void Cleanup() + { + Activity.Current = null; + } +} diff --git a/Benchmark.NetCore/GaugeBenchmarks.cs b/Benchmark.NetCore/GaugeBenchmarks.cs new file mode 100644 index 00000000..6de13456 --- /dev/null +++ b/Benchmark.NetCore/GaugeBenchmarks.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +public class GaugeBenchmarks +{ + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + private readonly Gauge _gauge; + + public GaugeBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + _gauge = _factory.CreateGauge("gauge", "help text"); + } + + [Benchmark] + public void SetToCurrenTimeUtc() + { + _gauge.SetToCurrentTimeUtc(); + } +} diff --git a/Benchmark.NetCore/HttpExporterBenchmarks.cs b/Benchmark.NetCore/HttpExporterBenchmarks.cs index b2970fbd..fd3729c4 100644 --- a/Benchmark.NetCore/HttpExporterBenchmarks.cs +++ b/Benchmark.NetCore/HttpExporterBenchmarks.cs @@ -2,61 +2,62 @@ using Microsoft.AspNetCore.Http; using Prometheus; using Prometheus.HttpMetrics; -using System.Threading.Tasks; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class HttpExporterBenchmarks { - [MemoryDiagnoser] - public class HttpExporterBenchmarks - { - private CollectorRegistry _registry; - private MetricFactory _factory; - private HttpInProgressMiddleware _inProgressMiddleware; - private HttpRequestCountMiddleware _countMiddleware; - private HttpRequestDurationMiddleware _durationMiddleware; + private CollectorRegistry _registry; + private MetricFactory _factory; + private HttpInProgressMiddleware _inProgressMiddleware; + private HttpRequestCountMiddleware _countMiddleware; + private HttpRequestDurationMiddleware _durationMiddleware; - [Params(1000, 10000)] - public int RequestCount { get; set; } + [Params(100_000)] + public int RequestCount { get; set; } + + [GlobalSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); - [GlobalSetup] - public void Setup() + _inProgressMiddleware = new HttpInProgressMiddleware(next => Task.CompletedTask, new HttpInProgressOptions { - _registry = Metrics.NewCustomRegistry(); - _factory = Metrics.WithCustomRegistry(_registry); - - _inProgressMiddleware = new HttpInProgressMiddleware(next => Task.CompletedTask, new HttpInProgressOptions - { - Gauge = _factory.CreateGauge("in_progress", "help") - }); - _countMiddleware = new HttpRequestCountMiddleware(next => Task.CompletedTask, new HttpRequestCountOptions - { - Counter = _factory.CreateCounter("count", "help") - }); - _durationMiddleware = new HttpRequestDurationMiddleware(next => Task.CompletedTask, new HttpRequestDurationOptions - { - Histogram = _factory.CreateHistogram("duration", "help") - }); - } - - [Benchmark] - public async Task HttpInProgress() + Gauge = _factory.CreateGauge("in_progress", "help") + }); + _countMiddleware = new HttpRequestCountMiddleware(next => Task.CompletedTask, new HttpRequestCountOptions { - for (var i = 0; i < RequestCount; i++) - await _inProgressMiddleware.Invoke(new DefaultHttpContext()); - } - - [Benchmark] - public async Task HttpRequestCount() + Counter = _factory.CreateCounter("count", "help") + }); + _durationMiddleware = new HttpRequestDurationMiddleware(next => Task.CompletedTask, new HttpRequestDurationOptions { - for (var i = 0; i < RequestCount; i++) - await _countMiddleware.Invoke(new DefaultHttpContext()); - } + Histogram = _factory.CreateHistogram("duration", "help") + }); + } - [Benchmark] - public async Task HttpRequestDuration() - { - for (var i = 0; i < RequestCount; i++) - await _durationMiddleware.Invoke(new DefaultHttpContext()); - } + // Reuse the same HttpContext for different requests, to not count its overhead in the benchmark. + private static readonly DefaultHttpContext _httpContext = new(); + + [Benchmark] + public async Task HttpInProgress() + { + for (var i = 0; i < RequestCount; i++) + await _inProgressMiddleware.Invoke(_httpContext); + } + + [Benchmark] + public async Task HttpRequestCount() + { + for (var i = 0; i < RequestCount; i++) + await _countMiddleware.Invoke(_httpContext); + } + + [Benchmark] + public async Task HttpRequestDuration() + { + for (var i = 0; i < RequestCount; i++) + await _durationMiddleware.Invoke(_httpContext); } } \ No newline at end of file diff --git a/Benchmark.NetCore/LabelBenchmarks.cs b/Benchmark.NetCore/LabelBenchmarks.cs deleted file mode 100644 index d8694e6c..00000000 --- a/Benchmark.NetCore/LabelBenchmarks.cs +++ /dev/null @@ -1,109 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Prometheus; - -namespace Benchmark.NetCore -{ - [MemoryDiagnoser] - public class LabelBenchmarks - { - // Metric -> Variant -> Label values - private static readonly string[][][] _labelValueRows; - - private const int _metricCount = 10; - private const int _variantCount = 10; - private const int _labelCount = 5; - - static LabelBenchmarks() - { - _labelValueRows = new string[_metricCount][][]; - - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - { - var variants = new string[_variantCount][]; - _labelValueRows[metricIndex] = variants; - - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) - { - var values = new string[_labelCount]; - _labelValueRows[metricIndex][variantIndex] = values; - - for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++) - values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}"; - } - } - } - - private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry(); - private readonly Counter[] _metrics; - - public LabelBenchmarks() - { - _metrics = new Counter[_metricCount]; - - var factory = Metrics.WithCustomRegistry(_registry); - - // Just use 1st variant for the keys (all we care about are that there is some name-like value in there). - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - _metrics[metricIndex] = factory.CreateCounter($"metric{metricIndex:D2}", "", _labelValueRows[metricIndex][0]); - } - - /// - /// Increments an unlabelled Collector instance for a single metric. - /// - [Benchmark] - public void WithoutLabels_OneMetric_OneSeries() - { - _metrics[0].Inc(); - } - - /// - /// Increments unlabelled Collector instances for a multiple metrics. - /// - [Benchmark] - public void WithoutLabels_ManyMetrics_OneSeries() - { - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - _metrics[metricIndex].Inc(); - } - - /// - /// Increments a labelled Collector.Child instance for a single metric. - /// - [Benchmark] - public void WithLabels_OneMetric_OneSeries() - { - _metrics[0].Labels(_labelValueRows[0][0]).Inc(); - } - - /// - /// Increments labelled Collector.Child instances for one metric with multiple different sets of labels. - /// - [Benchmark] - public void WithLabels_OneMetric_ManySeries() - { - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) - _metrics[0].Labels(_labelValueRows[0][variantIndex]).Inc(); - } - - /// - /// Increments a labelled Collector.Child instance for multiple metrics. - /// - [Benchmark] - public void WithLabels_ManyMetrics_OneSeries() - { - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - _metrics[metricIndex].Labels(_labelValueRows[metricIndex][0]).Inc(); - } - - /// - /// Increments labelled Collector.Child instances for multiple metrics with multiple different sets of labels. - /// - [Benchmark] - public void WithLabels_ManyMetrics_ManySeries() - { - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) - _metrics[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); - } - } -} diff --git a/Benchmark.NetCore/LabelSequenceBenchmarks.cs b/Benchmark.NetCore/LabelSequenceBenchmarks.cs new file mode 100644 index 00000000..5affa893 --- /dev/null +++ b/Benchmark.NetCore/LabelSequenceBenchmarks.cs @@ -0,0 +1,19 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class LabelSequenceBenchmarks +{ + private static readonly StringSequence Names3Array = StringSequence.From(new[] { "aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc" }); + private static readonly StringSequence Values3Array = StringSequence.From(new[] { "valueaaaaaaaaaaaaaaaaa", "valuebbbbbbbbbbbbbb", "valuecccccccccccccc" }); + + [Benchmark] + public void Create_From3Array() + { + // This is too fast for the benchmark engine, so let's create some additional work by looping through it many times. + for (var i = 0; i < 10_000; i++) + LabelSequence.From(Names3Array, Values3Array); + } +} diff --git a/Benchmark.NetCore/ManualDelayer.cs b/Benchmark.NetCore/ManualDelayer.cs new file mode 100644 index 00000000..20897c7b --- /dev/null +++ b/Benchmark.NetCore/ManualDelayer.cs @@ -0,0 +1,55 @@ +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// A delayer implementation that only returns from a delay when commanded to. +/// +internal sealed class ManualDelayer : IDelayer, IDisposable +{ + public void BreakAllDelays() + { + lock (_lock) + { + _tcs.TrySetResult(); + + _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + _delayTask = _tcs.Task; + } + } + + public void Dispose() + { + lock (_lock) + { + // If anything is still waiting, it shall not wait no more. + _tcs.TrySetResult(); + + // If anything will still wait in the future, it shall not wait at all. + // Beware of creating spinning loops if you dispose the delayer when something still expects to be delayed. + _delayTask = Task.CompletedTask; + } + } + + private readonly object _lock = new(); + private Task _delayTask; + private TaskCompletionSource _tcs; + + public ManualDelayer() + { + _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + _delayTask = _tcs.Task; + } + + public Task Delay(TimeSpan duration) + { + lock (_lock) + return _delayTask; + } + + public Task Delay(TimeSpan duration, CancellationToken cancel) + { + lock (_lock) + return _delayTask.WaitAsync(cancel); + } +} diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs new file mode 100644 index 00000000..b24bd49b --- /dev/null +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -0,0 +1,187 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// We take a bunch of measurements of each type of metric and show the cost. +/// +[MemoryDiagnoser] +public class MeasurementBenchmarks +{ + [Params(100_000)] + public int MeasurementCount { get; set; } + + [Params(ExemplarMode.Auto, ExemplarMode.None, ExemplarMode.Provided)] + public ExemplarMode Exemplars { get; set; } + + public enum ExemplarMode + { + /// + /// No user-supplied exemplar but the default behavior is allowed to execute (and fail to provide an exemplar). + /// + Auto, + + /// + /// Explicitly indicating that no exemplar is to be used. + /// + None, + + /// + /// Explicitly providing an exemplar. + /// + Provided + } + + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + + private readonly Counter.Child _counter; + private readonly Gauge.Child _gauge; + private readonly Summary.Child _summary; + private readonly Histogram.Child _histogram; + private readonly Histogram.Child _wideHistogram; + + private readonly Exemplar.LabelKey _traceIdKey = Exemplar.Key("trace_id"); + private readonly Exemplar.LabelKey _spanIdKey = Exemplar.Key("span_id"); + + // We preallocate the exemplar values to avoid measuring the random()->string serialization as part of the benchmark. + // What we care about measuring is the overhead of processing the exemplar, not of generating/serializing it. + private readonly string _traceIdValue = "7f825eb926a90af6961ace5f9a239945"; + private readonly string _spanIdValue = "a77603af408a13ec"; + + private Exemplar.LabelPair _traceIdLabel; + private Exemplar.LabelPair _spanIdLabel; + + /// + /// The max value we observe for histograms, to give us coverage of all the histogram buckets + /// but not waste 90% of the benchmark on incrementing the +Inf bucket. + /// + private const int WideHistogramMaxValue = 32 * 1024; + + // Same but for the regular histogram. + private readonly int _regularHistogramMaxValue; + + private static readonly string[] labelNames = ["label"]; + + public MeasurementBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + // We add a label to each, as labeled usage is the typical usage. + var counterTemplate = _factory.CreateCounter("counter", "test counter", labelNames); + var gaugeTemplate = _factory.CreateGauge("gauge", "test gauge", labelNames); + var summaryTemplate = _factory.CreateSummary("summary", "test summary", labelNames, new SummaryConfiguration + { + Objectives = new QuantileEpsilonPair[] + { + new(0.9, 0.1), + new(0.95, 0.01), + new(0.99, 0.005) + } + }); + + // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default. + var regularHistogramBuckets = Prometheus.Histogram.ExponentialBuckets(0.001, 2, 16); + + // Last one is +inf, so take the second-to-last. + _regularHistogramMaxValue = (int)regularHistogramBuckets[^2]; + + var histogramTemplate = _factory.CreateHistogram("histogram", "test histogram", labelNames, new HistogramConfiguration + { + Buckets = regularHistogramBuckets + }); + + var wideHistogramTemplate = _factory.CreateHistogram("wide_histogram", "test histogram", labelNames, new HistogramConfiguration + { + Buckets = Prometheus.Histogram.LinearBuckets(1, WideHistogramMaxValue / 128, 128) + }); + + // We cache the children, as is typical usage. + _counter = counterTemplate.WithLabels("label value"); + _gauge = gaugeTemplate.WithLabels("label value"); + _summary = summaryTemplate.WithLabels("label value"); + _histogram = histogramTemplate.WithLabels("label value"); + _wideHistogram = wideHistogramTemplate.WithLabels("label value"); + + // We take a single measurement, to warm things up and avoid any first-call impact. + _counter.Inc(); + _gauge.Set(1); + _summary.Observe(1); + _histogram.Observe(1); + _wideHistogram.Observe(1); + } + + [GlobalSetup] + public void GlobalSetup() + { + // There is an unavoidable string->bytes encoding overhead from this. + // As it is fixed overhead based on user data size, we pre-encode the strings here to avoid them influencing the benchmark results. + // We only preallocate the strings, however (creating the LabelPairs). We still do as much of the exemplar "processing" inline as feasible, to be realistic. + _traceIdLabel = _traceIdKey.WithValue(_traceIdValue); + _spanIdLabel = _spanIdKey.WithValue(_spanIdValue); + } + + [Benchmark] + public void Counter() + { + var exemplarProvider = GetExemplarProvider(); + + for (var i = 0; i < MeasurementCount; i++) + { + _counter.Inc(exemplarProvider()); + } + } + + [Benchmark] + public void Gauge() + { + for (var i = 0; i < MeasurementCount; i++) + { + _gauge.Set(i); + } + } + + [Benchmark] + public void Histogram() + { + var exemplarProvider = GetExemplarProvider(); + + for (var i = 0; i < MeasurementCount; i++) + { + var value = i % _regularHistogramMaxValue; + _histogram.Observe(value, exemplarProvider()); + } + } + + [Benchmark] + public void WideHistogram() + { + var exemplarProvider = GetExemplarProvider(); + + for (var i = 0; i < MeasurementCount; i++) + { + var value = i % WideHistogramMaxValue; + _wideHistogram.Observe(value, exemplarProvider()); + } + } + + // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage. + //[Benchmark] + public void Summary() + { + for (var i = 0; i < MeasurementCount; i++) + { + _summary.Observe(i); + } + } + + private Func GetExemplarProvider() => Exemplars switch + { + ExemplarMode.Auto => () => null, + ExemplarMode.None => () => Exemplar.None, + ExemplarMode.Provided => () => Exemplar.From(_traceIdLabel, _spanIdLabel), + _ => throw new NotImplementedException(), + }; +} diff --git a/Benchmark.NetCore/MeterAdapterBenchmarks.cs b/Benchmark.NetCore/MeterAdapterBenchmarks.cs new file mode 100644 index 00000000..b3abf76f --- /dev/null +++ b/Benchmark.NetCore/MeterAdapterBenchmarks.cs @@ -0,0 +1,95 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; +using SDM = System.Diagnostics.Metrics; + +namespace Benchmark.NetCore; + +/// +/// Equivalent of MeasurementBenchmarks, except we publish the data via .NET Meters API and convert via MeterAdapter. +/// +[MemoryDiagnoser] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] +public class MeterAdapterBenchmarks +{ + [Params(100_000)] + public int MeasurementCount { get; set; } + + private readonly SDM.Meter _meter = new("prometheus-net benchmark"); + private readonly SDM.Counter _intCounter; + private readonly SDM.Counter _floatCounter; + private readonly SDM.Histogram _intHistogram; + private readonly SDM.Histogram _floatHistogram; + + private readonly CollectorRegistry _registry; + + private readonly IDisposable _meterAdapter; + + private readonly KeyValuePair _label = new("label", "label value"); + + public MeterAdapterBenchmarks() + { + _intCounter = _meter.CreateCounter("int_counter", description: "This is an integer counter."); + _floatCounter = _meter.CreateCounter("float_counter", description: "This is a floating-point counter."); + _intHistogram = _meter.CreateHistogram("int_histogram", description: "This is an integer histogram."); + _floatHistogram = _meter.CreateHistogram("float_histogram", description: "This is a floating-point histogram."); + + _registry = Metrics.NewCustomRegistry(); + + _meterAdapter = MeterAdapter.StartListening(new MeterAdapterOptions + { + InstrumentFilterPredicate = instrument => instrument.Meter == _meter, + Registry = _registry, + // We resolve a custom set of buckets here, for maximum stress and to avoid easily reused default buckets. + // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default. + ResolveHistogramBuckets = _ => Histogram.ExponentialBuckets(0.001, 2, 16) + }); + + // We take a single measurement, to warm things up and avoid any first-call impact. + _intCounter.Add(1, _label); + _floatCounter.Add(1, _label); + _intHistogram.Record(1, _label); + _floatHistogram.Record(1, _label); + } + + [GlobalCleanup] + public void Cleanup() + { + _meterAdapter.Dispose(); + } + + [Benchmark] + public void CounterInt() + { + for (var i = 0; i < MeasurementCount; i++) + { + _intCounter.Add(1, _label); + } + } + + [Benchmark] + public void CounterFloat() + { + for (var i = 0; i < MeasurementCount; i++) + { + _floatCounter.Add(1, _label); + } + } + + [Benchmark] + public void HistogramInt() + { + for (var i = 0; i < MeasurementCount; i++) + { + _intHistogram.Record(i, _label); + } + } + + [Benchmark] + public void HistogramFloat() + { + for (var i = 0; i < MeasurementCount; i++) + { + _floatHistogram.Record(i, _label); + } + } +} diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index eb49c38b..09028762 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -1,207 +1,197 @@ using BenchmarkDotNet.Attributes; using Prometheus; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +/// +/// One pattern advocated by Prometheus documentation is to implement scraping of external systems by +/// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. +/// +[MemoryDiagnoser] +// This seems to need a lot of warmup to stabilize. +[WarmupCount(50)] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] +public class MetricCreationBenchmarks { /// - /// One pattern advocated by Prometheus documentation is to implement scraping of external systems by - /// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. + /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. /// - [MemoryDiagnoser] - public class MetricCreationBenchmarks - { - /// - /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. - /// - private const int _metricCount = 10000; + private const int _metricCount = 10_000; - /// - /// Some benchmarks try to register metrics that already exist. - /// - private const int _duplicateCount = 5; + /// + /// How many times we repeat acquiring and incrementing the same instance. + /// + [Params(1, 10)] + public int RepeatCount { get; set; } - private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + [Params(true, false)] + public bool IncludeStaticLabels { get; set; } - private static readonly string[] _metricNames; + private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; - static MetricCreationBenchmarks() - { - _metricNames = new string[_metricCount]; + private static readonly string[] _metricNames; - for (var i = 0; i < _metricCount; i++) - _metricNames[i] = $"metric_{i:D4}"; - } + static MetricCreationBenchmarks() + { + _metricNames = new string[_metricCount]; - private CollectorRegistry _registry; - private MetricFactory _factory; + for (var i = 0; i < _metricCount; i++) + _metricNames[i] = $"metric_{i:D4}"; + } - [IterationSetup] - public void Setup() - { - _registry = Metrics.NewCustomRegistry(); - _factory = Metrics.WithCustomRegistry(_registry); - } + private CollectorRegistry _registry; + private IMetricFactory _factory; + private IManagedLifetimeMetricFactory _managedLifetimeFactory; - private static readonly string[] _labelNames = new[] { "foo", "bar", "baz" }; + [IterationSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); - [Benchmark] - public void Counter_Many() + if (IncludeStaticLabels) { - for (var i = 0; i < _metricCount; i++) - _factory.CreateCounter(_metricNames[i], _help, new CounterConfiguration - { - LabelNames = _labelNames - }).Inc(); + _registry.SetStaticLabels(new Dictionary + { + { "static_foo", "static_bar" }, + { "static_foo1", "static_bar" }, + { "static_foo2", "static_bar" }, + { "static_foo3", "static_bar" }, + { "static_foo4", "static_bar" } + }); + + _factory = _factory.WithLabels(new Dictionary + { + { "static_gaa", "static_bar" }, + { "static_gaa1", "static_bar" }, + { "static_gaa2", "static_bar" }, + { "static_gaa3", "static_bar" }, + { "static_gaa4", "static_bar" }, + { "static_gaa5", "static_bar" }, + }); } - [Benchmark] - public void Gauge_Many() - { - for (var i = 0; i < _metricCount; i++) - _factory.CreateGauge(_metricNames[i], _help, new GaugeConfiguration - { - LabelNames = _labelNames - }).Inc(); - } + _managedLifetimeFactory = _factory.WithManagedLifetime(expiresAfter: TimeSpan.FromHours(1)); + } - [Benchmark] - public void Summary_Many() - { - for (var i = 0; i < _metricCount; i++) - _factory.CreateSummary(_metricNames[i], _help, new SummaryConfiguration - { - LabelNames = _labelNames - }).Observe(123); - } + // We use the same strings both for the names and the values. + private static readonly string[] _labels = ["foo", "bar", "baz"]; - [Benchmark] - public void Histogram_Many() - { - for (var i = 0; i < _metricCount; i++) - _factory.CreateHistogram(_metricNames[i], _help, new HistogramConfiguration - { - LabelNames = _labelNames - }).Observe(123); - } + private static readonly CounterConfiguration _counterConfiguration = CounterConfiguration.Default; + private static readonly GaugeConfiguration _gaugeConfiguration = GaugeConfiguration.Default; + private static readonly SummaryConfiguration _summaryConfiguration = SummaryConfiguration.Default; + private static readonly HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default; - [Benchmark] - public void Counter_Many_Duplicates() + [Benchmark] + public void Counter_ArrayLabels() + { + for (var i = 0; i < _metricCount; i++) { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - _factory.CreateCounter(_metricNames[i], _help, new CounterConfiguration - { - LabelNames = _labelNames - }).Inc(); - } + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); - [Benchmark] - public void Gauge_Many_Duplicates() - { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - _factory.CreateGauge(_metricNames[i], _help, new GaugeConfiguration - { - LabelNames = _labelNames - }).Inc(); + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(_labels).Inc(); } + } - [Benchmark] - public void Summary_Many_Duplicates() - { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - _factory.CreateSummary(_metricNames[i], _help, new SummaryConfiguration - { - LabelNames = _labelNames - }).Observe(123); - } + [Benchmark] + public void Counter_MemoryLabels() + { + // The overloads accepting string[] and ROM are functionally equivalent, though any conversion adds some overhead. + var labelsMemory = _labels.AsMemory(); - [Benchmark] - public void Histogram_Many_Duplicates() + for (var i = 0; i < _metricCount; i++) { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - _factory.CreateHistogram(_metricNames[i], _help, new HistogramConfiguration - { - LabelNames = _labelNames - }).Observe(123); - } + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); - [Benchmark] - public void Counter() - { - _factory.CreateCounter(_metricNames[0], _help, new CounterConfiguration - { - LabelNames = _labelNames - }).Inc(); + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(labelsMemory).Inc(); } + } - [Benchmark] - public void Gauge() + [Benchmark] + public void Counter_SpanLabels() + { + var labelsSpan = _labels.AsSpan(); + + for (var i = 0; i < _metricCount; i++) { - _factory.CreateGauge(_metricNames[0], _help, new GaugeConfiguration - { - LabelNames = _labelNames - }).Inc(); + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + // If code is aware that it is repeating the registration, using the Span overloads offers optimal performance. + metric.WithLabels(labelsSpan).Inc(); } + } + + [Benchmark] + public void Counter_ManagedLifetime() + { + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelsSpan = _labels.AsSpan(); - [Benchmark] - public void Summary() + for (var i = 0; i < _metricCount; i++) { - _factory.CreateSummary(_metricNames[0], _help, new SummaryConfiguration - { - LabelNames = _labelNames - }).Observe(123); + var metric = _managedLifetimeFactory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLease(static x => x.Inc(), labelsSpan); } + } - [Benchmark] - public void Histogram() - { - _factory.CreateHistogram(_metricNames[0], _help, new HistogramConfiguration + [Benchmark] + public void Counter_10Duplicates() + { + // We try to register the same metric 10 times, which is wasteful but shows us the overhead from doing this. + + for (var dupe = 0; dupe < 10; dupe++) + for (var i = 0; i < _metricCount; i++) { - LabelNames = _labelNames - }).Observe(123); - } + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); - [Benchmark] - public void Counter_Duplicates() - { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - _factory.CreateCounter(_metricNames[0], _help, new CounterConfiguration - { - LabelNames = _labelNames - }).Inc(); - } + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Inc(); + } + } - [Benchmark] - public void Gauge_Duplicates() + [Benchmark] + public void Gauge() + { + for (var i = 0; i < _metricCount; i++) { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - _factory.CreateGauge(_metricNames[0], _help, new GaugeConfiguration - { - LabelNames = _labelNames - }).Inc(); + var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Set(repeat); } + } - [Benchmark] - public void Summary_Duplicates() + // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage. + //[Benchmark] + public void Summary() + { + for (var i = 0; i < _metricCount; i++) { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - _factory.CreateSummary(_metricNames[0], _help, new SummaryConfiguration - { - LabelNames = _labelNames - }).Observe(123); + var metric = _factory.CreateSummary(_metricNames[i], _help, _labels, _summaryConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Observe(123); } + } - [Benchmark] - public void Histogram_Duplicates() + [Benchmark] + public void Histogram() + { + for (var i = 0; i < _metricCount; i++) { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - _factory.CreateHistogram(_metricNames[0], _help, new HistogramConfiguration - { - LabelNames = _labelNames - }).Observe(123); + var metric = _factory.CreateHistogram(_metricNames[i], _help, _labels, _histogramConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Observe(123); } } } diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs new file mode 100644 index 00000000..883ad131 --- /dev/null +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -0,0 +1,181 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// Here we try to ensure that creating/using expiring metrics does not impose too heavy of a performance burden or create easily identifiable memory leaks. +/// +[MemoryDiagnoser] +// This seems to need a lot of warmup to stabilize. +[WarmupCount(50)] +// This seems to need a lot of iterations to stabilize. +[IterationCount(50)] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] +public class MetricExpirationBenchmarks +{ + /// + /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. + /// + private const int _metricCount = 25_000; + + /// + /// If true, we preallocate a lifetime for every metric, so the benchmark only measures the actual usage + /// of the metric and not the first-lease setup, as these two components of a metric lifetime can have different impact in different cases. + /// + [Params(true, false)] + public bool PreallocateLifetime { get; set; } + + private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + + private static readonly string[] _metricNames; + + static MetricExpirationBenchmarks() + { + _metricNames = new string[_metricCount]; + + for (var i = 0; i < _metricCount; i++) + _metricNames[i] = $"metric_{i:D4}"; + } + + private CollectorRegistry _registry; + private IManagedLifetimeMetricFactory _factory; + + // We use the same strings both for the names and the values. + private static readonly string[] _labels = ["foo", "bar", "baz"]; + + private ManualDelayer _delayer; + + private readonly ManagedLifetimeMetricHandle[] _counters = new ManagedLifetimeMetricHandle[_metricCount]; + + [IterationSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry) + // We enable lifetime management but set the expiration timer very high to avoid expiration in the middle of the benchmark. + .WithManagedLifetime(expiresAfter: TimeSpan.FromHours(24)); + + _delayer = new(); + + for (var i = 0; i < _metricCount; i++) + { + var counter = CreateCounter(_metricNames[i], _help, _labels); + _counters[i] = counter; + + // Both the usage and the lifetime allocation matter but we want to bring them out separately in the benchmarks. + if (PreallocateLifetime) + counter.AcquireRefLease(out _, _labels).Dispose(); + } + } + + [IterationCleanup] + public void Cleanup() + { + // Ensure that all metrics are marked as expired, so the expiration processing logic destroys them all. + // This causes some extra work during cleanup but on the other hand, it ensures good isolation between iterations, so fine. + foreach (var counter in _counters) + counter.SetAllKeepaliveTimestampsToDistantPast(); + + // Twice and with some sleep time, just for good measure. + // BenchmarkDotNet today does not support async here, so we do a sleep to let the reaper thread process things. + _delayer.BreakAllDelays(); + Thread.Sleep(millisecondsTimeout: 5); + } + + private ManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labels) + { + var counter = (ManagedLifetimeMetricHandle)_factory.CreateCounter(name, help, labels); + + // We use a breakable delayer to ensure that we can control when the metric expiration logic runs, so one iteration + // of the benchmark does not start to interfere with another iteration just because some timers are left running. + counter.Delayer = _delayer; + + return counter; + } + + [Benchmark] + public void Use_AutoLease_Once() + { + for (var i = 0; i < _metricCount; i++) + { + var wrapper = _counters[i].WithExtendLifetimeOnUse(); + + // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed. + // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists). + wrapper.WithLabels(_labels).Inc(); + } + } + + [Benchmark] + public void Use_AutoLease_With10Duplicates() + { + for (var dupe = 0; dupe < 10; dupe++) + for (var i = 0; i < _metricCount; i++) + { + var wrapper = _counters[i].WithExtendLifetimeOnUse(); + + // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed. + // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists). + wrapper.WithLabels(_labels).Inc(); + } + } + + [Benchmark] + public void Use_AutoLease_Once_With10Repeats() + { + for (var i = 0; i < _metricCount; i++) + { + var wrapper = _counters[i].WithExtendLifetimeOnUse(); + + for (var repeat = 0; repeat < 10; repeat++) + // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed. + // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists). + wrapper.WithLabels(_labels).Inc(); + } + } + + [Benchmark(Baseline = true)] + public void Use_ManualLease() + { + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelValues = _labels.AsSpan(); + + for (var i = 0; i < _metricCount; i++) + { + using var lease = _counters[i].AcquireLease(out var instance, labelValues); + instance.Inc(); + } + } + + [Benchmark] + public void Use_ManualRefLease() + { + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelValues = _labels.AsSpan(); + + for (var i = 0; i < _metricCount; i++) + { + using var lease = _counters[i].AcquireRefLease(out var instance, labelValues); + instance.Inc(); + } + } + + private static void IncrementCounter(ICounter counter) + { + counter.Inc(); + } + + [Benchmark] + public void Use_WithLease() + { + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelValues = _labels.AsSpan(); + + // Reuse the delegate. + Action incrementCounterAction = IncrementCounter; + + for (var i = 0; i < _metricCount; i++) + _counters[i].WithLease(incrementCounterAction, labelValues); + } +} diff --git a/Benchmark.NetCore/MetricLookupBenchmarks.cs b/Benchmark.NetCore/MetricLookupBenchmarks.cs new file mode 100644 index 00000000..0c4f9572 --- /dev/null +++ b/Benchmark.NetCore/MetricLookupBenchmarks.cs @@ -0,0 +1,111 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// This benchmark aims to show that accessing preexisting metrics using various label combinations is fast and does not allocate memory. +/// +[MemoryDiagnoser] +public class MetricLookupBenchmarks +{ + // Metric -> Variant -> Label values + private static readonly string[][][] _labelValueRows; + + private const int _metricCount = 10; + private const int _variantCount = 10; + private const int _labelCount = 5; + + static MetricLookupBenchmarks() + { + _labelValueRows = new string[_metricCount][][]; + + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + { + var variants = new string[_variantCount][]; + _labelValueRows[metricIndex] = variants; + + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) + { + var values = new string[_labelCount]; + _labelValueRows[metricIndex][variantIndex] = values; + + for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++) + values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}"; + } + } + } + + private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry(); + private readonly Counter[] _metrics; + + public MetricLookupBenchmarks() + { + _metrics = new Counter[_metricCount]; + + var factory = Metrics.WithCustomRegistry(_registry); + + // Just use 1st variant for the keys (all we care about are that there is some name-like value in there). + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + _metrics[metricIndex] = factory.CreateCounter($"metric{metricIndex:D2}", "", _labelValueRows[metricIndex][0]); + } + + /// + /// Increments an unlabelled Collector instance for a single metric. + /// + [Benchmark] + public void WithoutLabels_OneMetric_OneSeries() + { + _metrics[0].Inc(); + } + + /// + /// Increments unlabelled Collector instances for a multiple metrics. + /// + [Benchmark] + public void WithoutLabels_ManyMetrics_OneSeries() + { + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + _metrics[metricIndex].Inc(); + } + + /// + /// Increments a labelled Collector.Child instance for a single metric. + /// + [Benchmark] + public void WithLabels_OneMetric_OneSeries() + { + _metrics[0].WithLabels(_labelValueRows[0][0]).Inc(); + } + + /// + /// Increments labelled Collector.Child instances for one metric with multiple different sets of labels. + /// + [Benchmark] + public void WithLabels_OneMetric_ManySeries() + { + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) + _metrics[0].WithLabels(_labelValueRows[0][variantIndex]).Inc(); + } + + /// + /// Increments a labelled Collector.Child instance for multiple metrics. + /// + [Benchmark] + public void WithLabels_ManyMetrics_OneSeries() + { + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + _metrics[metricIndex].WithLabels(_labelValueRows[metricIndex][0]).Inc(); + } + + /// + /// Increments labelled Collector.Child instances for multiple metrics with multiple different sets of labels. + /// + [Benchmark] + public void WithLabels_ManyMetrics_ManySeries() + { + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) + _metrics[metricIndex].WithLabels(_labelValueRows[metricIndex][variantIndex]).Inc(); + } +} diff --git a/Benchmark.NetCore/MetricPusherBenchmarks.cs b/Benchmark.NetCore/MetricPusherBenchmarks.cs index a4ca6f2d..83943efa 100644 --- a/Benchmark.NetCore/MetricPusherBenchmarks.cs +++ b/Benchmark.NetCore/MetricPusherBenchmarks.cs @@ -1,86 +1,78 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using Prometheus; using tester; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +// NB! This benchmark requires the Tester project to be running and the MetricPusherTester module to be active (to receive the data). +// If there is no tester listening, the results will be overly good because the runtime is under less I/O load. +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Monitoring, warmupCount: 0)] +public class MetricPusherBenchmarks { - // NB! This benchmark requires the Tester project to be running and the MetricPusherTester module to be active (to receive the data). - // If there is no tester listening, the results will be overly good because the runtime is under less I/O load. - [MemoryDiagnoser] - [SimpleJob(RunStrategy.Monitoring, warmupCount:0)] - public class MetricPusherBenchmarks + private static string MetricPusherUrl = $"http://localhost:{TesterConstants.TesterPort}"; + + [GlobalSetup] + public async Task GlobalSetup() { - private static string MetricPusherUrl = $"http://localhost:{TesterConstants.TesterPort}"; + // Verify that there is a MetricPusher listening on Tester. - [GlobalSetup] - public async Task GlobalSetup() + using (var client = new HttpClient()) { - // Verify that there is a MetricPusher listening on Tester. - - using (var client = new HttpClient()) + try { - try - { - var result = await client.GetAsync(MetricPusherUrl); - result.EnsureSuccessStatusCode(); - } - catch - { - throw new Exception("You must start the Tester.NetCore project and configure it to use MetricPusherTester in its Program.cs before running this benchmark."); - } + var result = await client.GetAsync(MetricPusherUrl); + result.EnsureSuccessStatusCode(); + } + catch + { + throw new Exception("You must start the Tester.NetCore project and configure it to use MetricPusherTester in its Program.cs before running this benchmark."); } } + } + - - [Benchmark] - public async Task PushTest() + [Benchmark] + public async Task PushTest() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var pusher = new MetricPusher(new MetricPusherOptions { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var pusher = new MetricPusher(new MetricPusherOptions - { - Endpoint = MetricPusherUrl, - Registry = registry, - IntervalMilliseconds = 30, - Job = "job" - }); - pusher.Start(); + Endpoint = MetricPusherUrl, + Registry = registry, + IntervalMilliseconds = 30, + Job = "job" + }); + pusher.Start(); - var counters = new List(); - for (int i = 0; i < 1000; i++) - { - var counter = factory.CreateCounter($"Counter{i}", String.Empty); - counters.Add(counter); - } + var counters = new List(); + for (int i = 0; i < 1000; i++) + { + var counter = factory.CreateCounter($"Counter{i}", String.Empty); + counters.Add(counter); + } - var ct = new CancellationTokenSource(); - var incTask = Task.Run(async () => + var ct = new CancellationTokenSource(); + var incTask = Task.Run(async () => + { + while (!ct.IsCancellationRequested) { - while (!ct.IsCancellationRequested) + foreach (var counter in counters) { - foreach (var counter in counters) - { - counter.Inc(); - } - - await Task.Delay(30); + counter.Inc(); } - }); - await Task.Delay(5000); - ct.Cancel(); - await incTask; + await Task.Delay(30); + } + }); - pusher.Stop(); - } + await Task.Delay(5000); + ct.Cancel(); + await incTask; + + pusher.Stop(); } } \ No newline at end of file diff --git a/Benchmark.NetCore/Program.cs b/Benchmark.NetCore/Program.cs index 9daa49ab..94822efb 100644 --- a/Benchmark.NetCore/Program.cs +++ b/Benchmark.NetCore/Program.cs @@ -1,14 +1,13 @@ using BenchmarkDotNet.Running; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +internal class Program { - internal class Program + private static void Main(string[] args) { - private static void Main(string[] args) - { - // Give user possibility to choose which benchmark to run. - // Can be overridden from the command line with the --filter option. - new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); - } + // Give user possibility to choose which benchmark to run. + // Can be overridden from the command line with the --filter option. + new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); } } diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs new file mode 100644 index 00000000..eb95dc2d --- /dev/null +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -0,0 +1,285 @@ +using System.Diagnostics.Metrics; +using BenchmarkDotNet.Attributes; +using OpenTelemetry.Metrics; +using Prometheus; + +namespace Benchmark.NetCore; + +/* +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2) +AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + Job-PPCGVJ : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Job | MaxIterationCount | Mean | Error | StdDev | Allocated | +|------------------------------ |----------- |------------------ |------------:|---------:|---------:|----------:| +| PromNetCounter | DefaultJob | Default | 230.4 μs | 1.20 μs | 1.12 μs | - | +| PromNetHistogram | DefaultJob | Default | 956.7 μs | 3.28 μs | 3.07 μs | - | +| OTelCounter | DefaultJob | Default | 10,998.2 μs | 35.54 μs | 31.51 μs | 11 B | +| OTelHistogram | DefaultJob | Default | 12,110.3 μs | 17.08 μs | 14.26 μs | 11 B | +| PromNetHistogramForAdHocLabel | Job-PPCGVJ | 16 | 716.2 μs | 30.20 μs | 26.77 μs | 664000 B | +| OTelHistogramForAdHocLabel | Job-PPCGVJ | 16 | 350.5 μs | 1.91 μs | 1.79 μs | 96000 B | +*/ + +/// +/// We compare pure measurement (not serializing the data) with prometheus-net SDK and OpenTelemetry .NET SDK. +/// +/// +/// Design logic: +/// * Metrics are initialized once on application startup. +/// * Metrics typically measure "sessions" - there are sets of metrics that are related through shared identifiers and a shared lifetime (e.g. HTTP request), +/// with all the identifiers for the metrics created when the sesison is initialized (e.g. when the HTTP connection is established). +/// * Metrics typically are also used report SLI (Service Level Indicator); these involve emitting a lot of unique dimension values, for example: CustomerId. +/// +/// Excluded from measurement: +/// * Meter setup (because meters are created once on application setup and not impactful later). +/// * Test data generation (session numbers and identifier strings) as it is SDK-neutral. +/// +/// We have a separate benchmark to compare the setup stage (just re-runs the setup logic in measured phase). +/// +/// We also do not benchmark "observable" metrics that are only polled at time of collection. +/// Both SDKs support it as an optimization (though OpenTelemetry forces it for counters) but let's try keep the logic here simple and exclude it for now. +/// +[MemoryDiagnoser] +public class SdkComparisonBenchmarks +{ + // Unique sets of label/tag values per metric. You can think of each one as a "session" we are reporting data for. + private const int TimeseriesPerMetric = 100; + + private static readonly string[] LabelNames = new[] { "environment", "server", "session_id" }; + private const string Label1Value = "production"; + private const string Label2Value = "hkhk298599-qps010-n200"; + + // How many observations we take during a single benchmark invocation (for each timeseries). + private const int ObservationCount = 1000; + + private static readonly string[] SessionIds = new string[TimeseriesPerMetric]; + + static SdkComparisonBenchmarks() + { + for (var i = 0; i < SessionIds.Length; i++) + SessionIds[i] = Guid.NewGuid().ToString(); + } + + /// + /// Contains all the context that gets initialized at iteration setup time. + /// + /// This data set is: + /// 1) Not included in the performance measurements. + /// 2) Reused for each invocation that is part of the same iteration. + /// + private abstract class MetricsContext : IDisposable + { + /// + /// Records an observation with all the counter-type metrics for each session. + /// + public abstract void ObserveCounter(double value); + + /// + /// Records an observation with all the histogram-type metrics for each session. + /// + public abstract void ObserveHistogram(double value); + + /// + /// Records an observation with one random label value as ad-hoc using a Histogram. + /// + public abstract void ObserveHistogramWithAnAdHocLabelValue(double value); + + public virtual void Dispose() { } + } + + private sealed class PrometheusNetMetricsContext : MetricsContext + { + private readonly List _counterInstances = new(TimeseriesPerMetric); + private readonly List _histogramInstances = new(TimeseriesPerMetric); + private readonly Histogram _histogramForAdHocLabels; + + private readonly IMetricServer _server; + + public PrometheusNetMetricsContext() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + // Do not emit any exemplars in this benchmark, as they are not yet equally supported by the SDKs. + factory.ExemplarBehavior = ExemplarBehavior.NoExemplars(); + + var counter = factory.CreateCounter("counter", "", LabelNames); + + for (var i = 0; i < TimeseriesPerMetric; i++) + _counterInstances.Add(counter.WithLabels(Label1Value, Label2Value, SessionIds[i])); + + var histogram = factory.CreateHistogram("histogram", "", LabelNames); + + _histogramForAdHocLabels = factory.CreateHistogram("histogramForAdHocLabels", "", LabelNames); + + for (var i = 0; i < TimeseriesPerMetric; i++) + _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i])); + + // `AddPrometheusHttpListener` of OpenTelemetry creates an HttpListener. + // Start an equivalent listener for Prometheus to ensure a fair comparison. + // We listen on 127.0.0.1 to avoid firewall prompts (randomly chosen port - we do not expect to receive any traffic). + _server = new MetricServer("127.0.0.1", port: 8051); + _server.Start(); + } + + public override void ObserveCounter(double value) + { + foreach (var counter in _counterInstances) + counter.Inc(value); + } + + public override void ObserveHistogram(double value) + { + foreach (var histogram in _histogramInstances) + histogram.Observe(value); + } + + public override void ObserveHistogramWithAnAdHocLabelValue(double value) + { + _histogramForAdHocLabels.WithLabels(Label1Value, Label2Value, Guid.NewGuid().ToString()).Observe(value); + } + + public override void Dispose() + { + base.Dispose(); + + _server.Dispose(); + } + } + + private sealed class OpenTelemetryMetricsContext : MetricsContext + { + private const string MeterBaseName = "benchmark"; + + private readonly Meter _meter; + private readonly MeterProvider _provider; + + private readonly Counter _counter; + private readonly Histogram _histogram; + private readonly Histogram _histogramForAdHocLabels; + + public OpenTelemetryMetricsContext() + { + // We use a randomized name every time because otherwise there appears to be some "shared state" between benchmark invocations, + // at least for the "setup" part which keeps getting slower every time we call it with the same metric name. + _meter = new Meter(MeterBaseName + Guid.NewGuid()); + + _counter = _meter.CreateCounter("counter"); + + _histogram = _meter.CreateHistogram("histogram"); + + _histogramForAdHocLabels = _meter.CreateHistogram("histogramForAdHocLabels"); + + _provider = OpenTelemetry.Sdk.CreateMeterProviderBuilder() + .AddView("histogram", new OpenTelemetry.Metrics.HistogramConfiguration() { RecordMinMax = false }) + .AddMeter(_meter.Name) + .AddPrometheusHttpListener() + .Build(); + } + + public override void ObserveCounter(double value) + { + for (int i = 0; i < SessionIds.Length; i++) + { + var tag1 = new KeyValuePair(LabelNames[0], Label1Value); + var tag2 = new KeyValuePair(LabelNames[1], Label2Value); + var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]); + _counter.Add(value, tag1, tag2, tag3); + } + } + + public override void ObserveHistogram(double value) + { + for (int i = 0; i < SessionIds.Length; i++) + { + var tag1 = new KeyValuePair(LabelNames[0], Label1Value); + var tag2 = new KeyValuePair(LabelNames[1], Label2Value); + var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]); + _histogram.Record(value, tag1, tag2, tag3); + } + } + + public override void ObserveHistogramWithAnAdHocLabelValue(double value) + { + var tag1 = new KeyValuePair(LabelNames[0], Label1Value); + var tag2 = new KeyValuePair(LabelNames[1], Label2Value); + var tag3 = new KeyValuePair(LabelNames[2], Guid.NewGuid().ToString()); + _histogramForAdHocLabels.Record(value, tag1, tag2, tag3); + } + + public override void Dispose() + { + base.Dispose(); + + _provider.Dispose(); + } + } + + private MetricsContext _context; + + [GlobalSetup(Targets = new string[] { nameof(OTelCounter), nameof(OTelHistogram), nameof(OTelHistogramForAdHocLabel) })] + public void OpenTelemetrySetup() + { + _context = new OpenTelemetryMetricsContext(); + } + + [GlobalSetup(Targets = new string[] { nameof(PromNetCounter), nameof(PromNetHistogram), nameof(PromNetHistogramForAdHocLabel) })] + public void PrometheusNetSetup() + { + _context = new PrometheusNetMetricsContext(); + } + + [Benchmark] + public void PromNetCounter() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveCounter(observation); + } + + [Benchmark] + public void PromNetHistogram() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogram(observation); + } + + [Benchmark] + [MaxIterationCount(16)] // Need to set a lower iteration count as this benchmarks allocates a lot memory and takes too long to complete with the default number of iterations. + public void PromNetHistogramForAdHocLabel() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogramWithAnAdHocLabelValue(observation); + } + + [Benchmark] + public void OTelCounter() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveCounter(observation); + } + + [Benchmark] + public void OTelHistogram() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogram(observation); + } + + [Benchmark] + [MaxIterationCount(16)] // Set the same number of iteration count as the corresponding PromNet benchmark. + public void OTelHistogramForAdHocLabel() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogramWithAnAdHocLabelValue(observation); + } + + [GlobalCleanup] + public void Cleanup() + { + _context.Dispose(); + } +} diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs index 288c2b6b..96bf2542 100644 --- a/Benchmark.NetCore/SerializationBenchmarks.cs +++ b/Benchmark.NetCore/SerializationBenchmarks.cs @@ -1,84 +1,205 @@ -using BenchmarkDotNet.Attributes; +using System.IO.Pipes; +using BenchmarkDotNet.Attributes; using Prometheus; -using System.IO; -using System.Threading.Tasks; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)] +public class SerializationBenchmarks { - [MemoryDiagnoser] - public class SerializationBenchmarks + public enum OutputStreamType { - // Metric -> Variant -> Label values - private static readonly string[][][] _labelValueRows; + /// + /// A null stream that just does nothing and immediately returns. + /// Low overhead but also unrealistic in terms of asynchronous I/O behavior. + /// + Null, + + /// + /// A stream that does nothing except yielding the task/thread to take up nonzero time. + /// Tries to increase the overhead from async/await task management that might occur. + /// + Yield, + + /// + /// A named pipe connection. Something halfway between null and a real network connection. + /// Named pipes appear to be super slow when dealing with small amounts of data, so optimizing + /// this scenario is valuable to ensure that we perform well with real network connections that + /// may have similar limitations (depending on OS and network stack). + /// + NamedPipe + } + + [Params(OutputStreamType.Null, OutputStreamType.NamedPipe, OutputStreamType.Yield)] + public OutputStreamType StreamType { get; set; } + + // Metric -> Variant -> Label values + private static readonly string[][][] _labelValueRows; + + private const int _metricCount = 100; + private const int _variantCount = 100; + private const int _labelCount = 5; - private const int _metricCount = 100; - private const int _variantCount = 100; - private const int _labelCount = 5; + private const string _help = "arbitrary help message for metric lorem ipsum dolor golor bolem"; - private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + static SerializationBenchmarks() + { + _labelValueRows = new string[_metricCount][][]; - static SerializationBenchmarks() + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) { - _labelValueRows = new string[_metricCount][][]; + var variants = new string[_variantCount][]; + _labelValueRows[metricIndex] = variants; - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) { - var variants = new string[_variantCount][]; - _labelValueRows[metricIndex] = variants; - - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) - { - var values = new string[_labelCount]; - _labelValueRows[metricIndex][variantIndex] = values; + var values = new string[_labelCount]; + _labelValueRows[metricIndex][variantIndex] = values; - for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++) - values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}"; - } + for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++) + values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}"; } } + } - private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry(); - private readonly Counter[] _counters; - private readonly Gauge[] _gauges; - private readonly Summary[] _summaries; - private readonly Histogram[] _histograms; + private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry(); + private readonly Counter[] _counters; + private readonly Gauge[] _gauges; + private readonly Summary[] _summaries; + private readonly Histogram[] _histograms; - public SerializationBenchmarks() - { - _counters = new Counter[_metricCount]; - _gauges = new Gauge[_metricCount]; - _summaries = new Summary[_metricCount]; - _histograms = new Histogram[_metricCount]; + public SerializationBenchmarks() + { + _counters = new Counter[_metricCount]; + _gauges = new Gauge[_metricCount]; + _summaries = new Summary[_metricCount]; + _histograms = new Histogram[_metricCount]; - var factory = Metrics.WithCustomRegistry(_registry); + var factory = Metrics.WithCustomRegistry(_registry); - // Just use 1st variant for the keys (all we care about are that there is some name-like value in there). - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + // Just use 1st variant for the keys (all we care about are that there is some name-like value in there). + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + { + _counters[metricIndex] = factory.CreateCounter($"counter{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _gauges[metricIndex] = factory.CreateGauge($"gauge{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + } + + // Genmerate some sample data so the metrics are not all zero-initialized. + var exemplarLabelPair = Exemplar.Key("traceID").WithValue("bar"); + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) { - _counters[metricIndex] = factory.CreateCounter($"counter{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - _gauges[metricIndex] = factory.CreateGauge($"gauge{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(Exemplar.From(exemplarLabelPair)); + _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); + _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); + _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex, Exemplar.From(exemplarLabelPair)); } + } + + [GlobalSetup] + public void Setup() + { + if (StreamType == OutputStreamType.Null) + { + _outputStream = Stream.Null; } + else if (StreamType == OutputStreamType.Yield) + { + _outputStream = YieldStream.Default; + } + else if (StreamType == OutputStreamType.NamedPipe) + { + var pipeName = StartStreamReader(); + + var pipeStream = new NamedPipeClientStream(".", pipeName.ToString(), PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + pipeStream.Connect(TimeSpan.FromSeconds(1)); + + _outputStream = pipeStream; + } + else + throw new NotSupportedException(); + } - [GlobalSetup] - public void GenerateData() + // Will be reused by all the iterations - the serializer does not take ownership nor close the stream. + private Stream _outputStream; + + // When this is cancelled, the output stream reader will stop listening for new connections and reading data from existing ones. + private readonly CancellationTokenSource _outputStreamReaderCts = new(); + + // We just read data into it, we do not care about the contents. + // While we do not expect concurrent access, it is fine if it does happen because this data is never consumed + private static readonly byte[] _readBuffer = new byte[1024]; + + /// + /// Starts listening on a random port on the loopback interface and returns the name of the created pipe stream. + /// + private Guid StartStreamReader() + { + var name = Guid.NewGuid(); + var server = new NamedPipeServerStream(name.ToString(), PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + var cancel = _outputStreamReaderCts.Token; + + _ = Task.Run(async delegate { - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) + try + { + while (!cancel.IsCancellationRequested) { - _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); - _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); - _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); - _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); + await server.WaitForConnectionAsync(cancel); + Console.WriteLine("Received a connection."); + + try + { + while (!cancel.IsCancellationRequested) + { + var bytesRead = await server.ReadAsync(_readBuffer, cancel); + + if (bytesRead == 0) + break; + } + } + finally + { + server.Disconnect(); + } } - } + } + catch (OperationCanceledException) when (cancel.IsCancellationRequested) + { + // Expected + } + catch (Exception ex) + { + Console.Error.WriteLine($"Unexpected exception in output stream reader: {ex}"); + } + finally + { + server.Dispose(); + } + }); - [Benchmark] - public async Task CollectAndSerialize() - { - await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); - } + return name; + } + + [Benchmark] + public async Task CollectAndSerialize() + { + await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream), default); + } + + [Benchmark] + public async Task CollectAndSerializeOpenMetrics() + { + await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream, ExpositionFormat.OpenMetricsText), default); + } + + [GlobalCleanup] + public void Cleanup() + { + _outputStreamReaderCts.Cancel(); + _outputStreamReaderCts.Dispose(); } } diff --git a/Benchmark.NetCore/StringSequenceBenchmarks.cs b/Benchmark.NetCore/StringSequenceBenchmarks.cs new file mode 100644 index 00000000..08e61c02 --- /dev/null +++ b/Benchmark.NetCore/StringSequenceBenchmarks.cs @@ -0,0 +1,76 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class StringSequenceBenchmarks +{ + private static readonly string[] Values3Array = ["aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc"]; + private static readonly ReadOnlyMemory Values3Memory = new(Values3Array); + + private static readonly string[] Values3ArrayPart1 = ["aaaaaaaaaaaaaaaaa"]; + private static readonly string[] Values3ArrayPart2 = ["bbbbbbbbbbbbbb"]; + private static readonly string[] Values3ArrayPart3 = ["cccccccccccccc"]; + + [Benchmark] + public void Create_From3Array() + { + StringSequence.From(Values3Array); + } + + [Benchmark] + public void Create_From3Memory() + { + StringSequence.From(Values3Memory); + } + + [Benchmark] + public void Create_From3ArrayConcat() + { + var part1 = StringSequence.From(Values3ArrayPart1); + var part2 = StringSequence.From(Values3ArrayPart2); + var part3 = StringSequence.From(Values3ArrayPart3); + + part1.Concat(part2).Concat(part3); + } + + private static readonly StringSequence FromValues3 = StringSequence.From(Values3Array); + private static readonly StringSequence Other = StringSequence.From(new[] { "fooooooooooooooo", "baaaaaaaaaaaaar", "baaaaaaaaaaz" }); + + [Benchmark] + public void Contains_Positive() + { + FromValues3.Contains(Values3Array[2]); + } + + [Benchmark] + public void Contains_Negative() + { + FromValues3.Contains("a string that is not in there"); + } + + [Benchmark] + public void Equals_Positive() + { + FromValues3.Equals(FromValues3); + } + + [Benchmark] + public void Equals_Negative() + { + FromValues3.Equals(Other); + } + + [Benchmark] + public void Concat_Empty() + { + FromValues3.Concat(StringSequence.Empty); + } + + [Benchmark] + public void Concat_ToEmpty() + { + StringSequence.Empty.Concat(FromValues3); + } +} diff --git a/Benchmark.NetCore/SummaryBenchmarks.cs b/Benchmark.NetCore/SummaryBenchmarks.cs index 1b64983c..1a42a91a 100644 --- a/Benchmark.NetCore/SummaryBenchmarks.cs +++ b/Benchmark.NetCore/SummaryBenchmarks.cs @@ -1,80 +1,76 @@ using BenchmarkDotNet.Attributes; using Prometheus; -using System; -using System.IO; -using System.Threading.Tasks; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +/// +/// Summary can be quite expensive to use due to its quantile measurement logic. +/// This benchmark helps get a grip on the facts. +/// +[MemoryDiagnoser] +public class SummaryBenchmarks { - /// - /// Summary can be quite expensive to use due to its quantile measurement logic. - /// This benchmark helps get a grip on the facts. - /// - [MemoryDiagnoser] - public class SummaryBenchmarks + // Arbitrary but reasonable objectices we might use for a summary. + private static readonly QuantileEpsilonPair[] Objectives = new[] { - // Arbitrary but reasonable objectices we might use for a summary. - private static readonly QuantileEpsilonPair[] Objectives = new[] - { - new QuantileEpsilonPair(0.5, 0.05), - new QuantileEpsilonPair(0.9, 0.01), - new QuantileEpsilonPair(0.95, 0.01), - new QuantileEpsilonPair(0.99, 0.005) - }; + new QuantileEpsilonPair(0.5, 0.05), + new QuantileEpsilonPair(0.9, 0.01), + new QuantileEpsilonPair(0.95, 0.01), + new QuantileEpsilonPair(0.99, 0.005) + }; - // We pre-generate some random data that we feed into the benchmark, to avoid measuring data generation. - private static readonly double[] Values = new double[1 * 1024 * 1024]; + // We pre-generate some random data that we feed into the benchmark, to avoid measuring data generation. + private static readonly double[] Values = new double[1 * 1024 * 1024]; - private static readonly TimeSpan ExportInterval = TimeSpan.FromMinutes(1); + private static readonly TimeSpan ExportInterval = TimeSpan.FromMinutes(1); - static SummaryBenchmarks() - { - var rnd = new Random(); + static SummaryBenchmarks() + { + var rnd = new Random(); - for (var i = 0; i < Values.Length; i++) - Values[i] = rnd.NextDouble(); - } + for (var i = 0; i < Values.Length; i++) + Values[i] = rnd.NextDouble(); + } - private CollectorRegistry _registry; - private MetricFactory _factory; + private CollectorRegistry _registry; + private MetricFactory _factory; - [IterationSetup] - public void Setup() - { - _registry = Metrics.NewCustomRegistry(); - _factory = Metrics.WithCustomRegistry(_registry); - } + [IterationSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + } - [Params(1, 10, 100, 1000, 10000)] - public int MeasurementsPerSecond { get; set; } + [Params(1, 10, 100, 1000, 10000)] + public int MeasurementsPerSecond { get; set; } - [Benchmark] - public async Task Summary_NPerSecond_For10Minutes() + [Benchmark] + public async Task Summary_NPerSecond_For10Minutes() + { + var summary = _factory.CreateSummary("metric_name", "help_string", new SummaryConfiguration { - var summary = _factory.CreateSummary("metric_name", "help_string", new SummaryConfiguration - { - Objectives = Objectives - }); + Objectives = Objectives + }); - var now = DateTime.UtcNow; + var now = DateTime.UtcNow; - // We start far enough back to cover the entire age range of the summary. - var t = now - Summary.DefMaxAge; - var lastExport = t; + // We start far enough back to cover the entire age range of the summary. + var t = now - Summary.DefMaxAge; + var lastExport = t; - while (t < now) - { - for (var i = 0; i < MeasurementsPerSecond; i++) - summary.Observe(Values[i % Values.Length]); + while (t < now) + { + for (var i = 0; i < MeasurementsPerSecond; i++) + summary.Observe(Values[i % Values.Length]); - t += TimeSpan.FromSeconds(1); + t += TimeSpan.FromSeconds(1); - if (lastExport + ExportInterval <= t) - { - lastExport = t; + if (lastExport + ExportInterval <= t) + { + lastExport = t; - await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); - } + await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), true, default); } } } diff --git a/Benchmark.NetCore/YieldStream.cs b/Benchmark.NetCore/YieldStream.cs new file mode 100644 index 00000000..7670c59e --- /dev/null +++ b/Benchmark.NetCore/YieldStream.cs @@ -0,0 +1,119 @@ +using System.Runtime.CompilerServices; + +namespace Benchmark.NetCore; + +/// +/// A stream that does nothing except yielding the task/thread to take up nonzero time. Modeled after NullStream. +/// +internal sealed class YieldStream : Stream +{ + public static readonly YieldStream Default = new(); + + private YieldStream() { } + + public override bool CanRead => true; + public override bool CanWrite => true; + public override bool CanSeek => true; + public override long Length => 0; + public override long Position { get => 0; set { } } + + public override void CopyTo(Stream destination, int bufferSize) + { + Thread.Yield(); + } + + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + protected override void Dispose(bool disposing) + { + // Do nothing - we don't want this stream to be closable. + } + + public override void Flush() + { + Thread.Yield(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + + public override int Read(byte[] buffer, int offset, int count) + { + Thread.Yield(); + return 0; + } + + public override int Read(Span buffer) + { + Thread.Yield(); + return 0; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + return 0; + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + return 0; + } + + public override int ReadByte() + { + Thread.Yield(); + return -1; + } + + public override void Write(byte[] buffer, int offset, int count) + { + Thread.Yield(); + } + + public override void Write(ReadOnlySpan buffer) + { + Thread.Yield(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + public override void WriteByte(byte value) + { + Thread.Yield(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + Thread.Yield(); + return 0; + } + + public override void SetLength(long length) + { + Thread.Yield(); + } +} diff --git a/Docs/MeasurementsBenchmarks.xlsx b/Docs/MeasurementsBenchmarks.xlsx new file mode 100644 index 00000000..f2b29726 Binary files /dev/null and b/Docs/MeasurementsBenchmarks.xlsx differ diff --git a/Exemplars.png b/Exemplars.png new file mode 100644 index 00000000..d199d655 Binary files /dev/null and b/Exemplars.png differ diff --git a/History b/History index 42ed855e..2c7d57cb 100644 --- a/History +++ b/History @@ -1,3 +1,57 @@ +* 8.2.1 +- Fix occasional "Collection was modified" exception when serializing metrics. #464 +* 8.2.0 +- .WithLabels() & similar now accept ReadOnlyMemory as alternative to string[]. Same behavior, just easier to use if you already have a ReadOnlyMemory. +- .WithLabels() & similar now accept ReadOnlySpan as alternative to string[]. This enables allocation-free metric instance creation if a metric instance with these labels is already known. +- Incorporated various optimizations to reduce the required CPU time and allocated memory, including #410, #443 and other contributions. +- Observation of large histograms is now 10-30% faster on machines that support AVX2 instructions. +- health checks exposed via ForwardToPrometheus() no longer create a default metric if a custom metric is provided #444 +* 8.1.1 +- Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453 +* 8.1.0 +- Add support for capturing HttpClient metrics from all registered HttpClients (`services.UseHttpClientMetrics()`). +* 8.0.1 +- Allow ObservableCounter to be reset. Previously, the MeterAdapter output got stuck on its previous maximum if the underlying Meter reset its value to a lower value. Now we transform the value 1:1. +* 8.0.0 +- Added OpenMetrics exposition format support (#388). +- Added exemplar support for Counter and Histogram (#388). +- The ':' character is no longer allowed in metric or label names. For metric names, Prometheus standard practice is to use colon only in recording rules. +- Publish symbol packages and add Source Link support for easier debugging experience. +- Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389). +- Ensure even harder that MetricPusher flushes the final state before stopping (#383 and #384) +- Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365, #364) +- Slightly improve Counter performance under concurrent load. +- Reduce memory allocations performed during ASP.NET Core HTTP request tracking. +- By default, EventCounterAdapter will only listen to a small predefined set of general-purpose useful event sources, to minimize resource consumption in the default configuration. A custom event source filter must now be provided to enable listening for additional event sources. +- EventCounterAdapter will only refresh data every 10 seconds by default, to reduce amount of garbage generated in memory (.NET event counters are very noisy and create many temporary objects). +- Added `IManagedLifetimeMetricFactory.WithLabels()` to enable simpler label enrichment in scenarios where lifetime-managed metric instances are used. +* 7.1.0 +- Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). +- Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). +* 7.0.0 +- .NET Core specific functionality now targeting .NET 6.0 or greater (all previous versions will be end of life by December 2022). +- Relaxed the API restriction that forbade you to create multiple metrics with the same name but different label names. While this is a Prometheus anti-pattern, it is a standard pattern in other metrics technologies and we will allow it in the name of interoperability. +- Added IMetricFactory.WithLabels() to define static labels per-factory and reduce the effort needed by users when defining static labels in specific scopes. +- Added automatic metric expiration feature, to remove timeseries when they are no longer used after a while. This improves interoperability with other metrics technologies such as .NET Meters. +- (Breaking change) Rewrite of the MeterAdapter functionality to bridge .NET 6 metrics to Prometheus. API remains equivalent, though options class changed. Output format (what metrics are published) is completely different. Now it translates metric names (instead of putting them in labels) and preserves tags (labels) provided via the .NET Meters API. Added sample project to demonstrate functionality. Ref #359 +- (Breaking change) Dropped support for .NET Standard 2.0 - now will target specifically .NET 6.0 and .NET Framework 4.6.2. This was already documented before but there was "unofficial" support also for .NET Standard remaining. This is now removed. +- (Breaking change) EventCounterAdapter output format changed to translate counters to Prometheus metrics and to improve usability by publishing both counters and gauges from ambiguous event counters. +- EventCounterAdapter and MeterAdapter are now enabled on startup by default. +- Added some debug metrics to indicate the number of metric families, instances and timeseries being exported by prometheus-net. +- Added typical benchmark results to readme for easy reference, and to show relative speed of the simple versus complex instruments. +- Removed MetricConfiguration.StaticLabels because it had negative performance impact on many code paths that did not use it. The same functionality can be manually implemented in user code. Static labels remain available on registry and factory level. +- When a before-collection metrics update callback throws an exception, it is now logged and ignored instead of breaking all metrics collection. +- EventCounterAdapter now defaults to Informational events only, for reduced performance overhead. Can be configured via callback. +* 6.0.0 +- Dropped library support for unsupported .NET runtime versions (.NET 2.1). +- If Razor Pages is enabled, the "page" label is now automatically added to the default HTTP server metrics, unless there is a user-defined label already present with this name. +- The "endpoint" label is now automatically added to the default HTTP server metrics, containing this endpoint routing route pattern of the request, unless there is a user-defined label already present with this name. +- Added integration with .NET EventCounters, which can now be easily exposed as Promtheus metrics (see EventCounterAdapter class). +- Added integration with .NET Meters, which can now be easily exposed as Promtheus metrics (see MeterAdapter class). +- Added ReplaceOnPush option to MetricPusher #305 #322. +- Minimum .NET Framework version is now 4.6.2. +* 5.1.0 +- Add support for adding custom label value providers to HTTP server metrics. #347 * 5.0.2 - Bugfix: HttpClientMetrics under concurrent load with long-running requests were leading to broken response streams. #336 * 5.0.1 diff --git a/LocalMetricsCollector/Dockerfile b/LocalMetricsCollector/Dockerfile new file mode 100644 index 00000000..61ff408b --- /dev/null +++ b/LocalMetricsCollector/Dockerfile @@ -0,0 +1,37 @@ +FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 + +# gettext is used by run.sh to do env var replacement +# ca-certificates-microsoft lets us access github.com for download +RUN tdnf repolist --refresh \ + && tdnf install -y gettext ca-certificates-microsoft + +# Install dependencies +RUN tdnf install -y \ + wget \ + tar \ + gzip + +################### Based on https://github.com/prometheus/prometheus/blob/main/Dockerfile + +ARG PROMETHEUS_PACKAGE_NAME=prometheus-2.41.0.linux-amd64 +RUN wget --no-verbose -O prometheus.tar.gz https://github.com/prometheus/prometheus/releases/download/v2.41.0/$PROMETHEUS_PACKAGE_NAME.tar.gz +RUN tar xvfz prometheus.tar.gz +RUN cp /$PROMETHEUS_PACKAGE_NAME/prometheus /bin/ +RUN cp /$PROMETHEUS_PACKAGE_NAME/promtool /bin/ +RUN mkdir /usr/share/prometheus +RUN cp -r /$PROMETHEUS_PACKAGE_NAME/console_libraries/ /usr/share/prometheus/console_libraries/ +RUN cp -r /$PROMETHEUS_PACKAGE_NAME/consoles/ /usr/share/prometheus/consoles/ + +RUN mkdir -p /prometheus && mkdir -p /etc/prometheus + +################### Custom logic start + +WORKDIR /app + +COPY run.sh . +RUN chmod +x /app/run.sh + +# This is the config template. run.sh will inject env variables into it. +COPY prometheus.yml . + +ENTRYPOINT ["/app/run.sh"] \ No newline at end of file diff --git a/LocalMetricsCollector/prometheus.yml b/LocalMetricsCollector/prometheus.yml new file mode 100644 index 00000000..885d1252 --- /dev/null +++ b/LocalMetricsCollector/prometheus.yml @@ -0,0 +1,28 @@ +global: + scrape_interval: 10s + evaluation_interval: 10s + scrape_timeout: 3s + +# Local Prometheus collector for testing and dashboard development purposes. + +remote_write: +- url: https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push + send_exemplars: true + basic_auth: + username: $GRAFANA_USER + password: $GRAFANA_API_KEY + +scrape_configs: + - job_name: prometheus-net + scheme: http + static_configs: + - targets: + # Most of the samples use 1234 + - $HOST_IP:1234 + # Sample.Grpc + - $HOST_IP:5025 + # Sample.Web + - $HOST_IP:5099 + # Sample.Web.MetricExpiration + - $HOST_IP:5283 + # Not supported: Sample.Web.NetFramework because IIS Express is localhost-only. \ No newline at end of file diff --git a/LocalMetricsCollector/run.sh b/LocalMetricsCollector/run.sh new file mode 100644 index 00000000..ab0c4e07 --- /dev/null +++ b/LocalMetricsCollector/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +# Replace any env variables in the template, yielding the final configuration file. +envsubst < /app/prometheus.yml > /etc/prometheus/prometheus.yml + +# We must listen on 0.0.0.0 here because otherwise the liveness/readiness probes cannot reach us. +exec /bin/prometheus --web.listen-address=0.0.0.0:$PROMETHEUS_PORT --storage.tsdb.retention.time=5m --storage.tsdb.min-block-duration=2m --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --web.console.libraries=/usr/share/prometheus/console_libraries --web.console.templates=/usr/share/prometheus/consoles --enable-feature=exemplar-storage \ No newline at end of file diff --git a/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs b/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs index 895066a1..e9cbcfea 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs @@ -1,36 +1,34 @@ -using System; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; -namespace Prometheus +namespace Prometheus; + +public static class GrpcMetricsMiddlewareExtensions { - public static class GrpcMetricsMiddlewareExtensions + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. + /// + public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, + Action configure) { - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. - /// - public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, - Action configure) - { - var options = new GrpcMiddlewareExporterOptions(); - configure?.Invoke(options); - app.UseGrpcMetrics(options); - return app; - } - - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. - /// - public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, - GrpcMiddlewareExporterOptions? options = null) - { - options ??= new GrpcMiddlewareExporterOptions(); + var options = new GrpcMiddlewareExporterOptions(); + configure?.Invoke(options); + app.UseGrpcMetrics(options); + return app; + } - if (options.RequestCount.Enabled) - { - app.UseMiddleware(options.RequestCount); - } + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. + /// + public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, + GrpcMiddlewareExporterOptions? options = null) + { + options ??= new GrpcMiddlewareExporterOptions(); - return app; + if (options.RequestCount.Enabled) + { + app.UseMiddleware(options.RequestCount); } + + return app; } } diff --git a/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs b/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs index 63f6056f..034478ea 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs @@ -1,13 +1,12 @@ -namespace Prometheus +namespace Prometheus; + +public abstract class GrpcMetricsOptionsBase { - public abstract class GrpcMetricsOptionsBase - { - public bool Enabled { get; set; } = true; + public bool Enabled { get; set; } = true; - /// - /// Allows you to override the registry used to create the default metric instance. - /// Value is ignored if you specify a custom metric instance in the options. - /// - public CollectorRegistry? Registry { get; set; } - } + /// + /// Allows you to override the registry used to create the default metric instance. + /// Value is ignored if you specify a custom metric instance in the options. + /// + public CollectorRegistry? Registry { get; set; } } diff --git a/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs b/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs index f7d65578..8254fc19 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs @@ -1,7 +1,6 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class GrpcMiddlewareExporterOptions { - public sealed class GrpcMiddlewareExporterOptions - { - public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions(); - } + public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions(); } diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs index 11211fa0..9eba9ec1 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs @@ -1,37 +1,31 @@ using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; -namespace Prometheus +namespace Prometheus; + +/// +/// Counts the number of requests to gRPC services. +/// +internal sealed class GrpcRequestCountMiddleware : GrpcRequestMiddlewareBase, ICounter> { - /// - /// Counts the number of requests to gRPC services. - /// - internal sealed class GrpcRequestCountMiddleware : GrpcRequestMiddlewareBase, ICounter> - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public GrpcRequestCountMiddleware(RequestDelegate next, GrpcRequestCountOptions? options) - : base(options, options?.Counter) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } + public GrpcRequestCountMiddleware(RequestDelegate next, GrpcRequestCountOptions? options) + : base(options, options?.Counter) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } - public async Task Invoke(HttpContext context) - { - CreateChild(context)?.Inc(); + public async Task Invoke(HttpContext context) + { + CreateChild(context)?.Inc(); - await _next(context); - } + await _next(context); + } - protected override string[] DefaultLabels => GrpcRequestLabelNames.All; + protected override string[] DefaultLabels => GrpcRequestLabelNames.All; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( - "grpc_requests_received_total", - "Number of gRPC requests received (including those currently being processed).", - new CounterConfiguration - { - LabelNames = labelNames - }); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( + "grpc_requests_received_total", + "Number of gRPC requests received (including those currently being processed).", + labelNames); } \ No newline at end of file diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs index d0fcc291..dd986ef2 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class GrpcRequestCountOptions : GrpcMetricsOptionsBase { - public sealed class GrpcRequestCountOptions : GrpcMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Counter { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Counter { get; set; } } diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs index ac17708e..95a79886 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs @@ -1,17 +1,16 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Reserved label names used in gRPC metrics. +/// +public static class GrpcRequestLabelNames { - /// - /// Reserved label names used in gRPC metrics. - /// - public static class GrpcRequestLabelNames - { - public const string Service = "service"; - public const string Method = "method"; + public const string Service = "service"; + public const string Method = "method"; - public static readonly string[] All = - { - Service, - Method, - }; - } + public static readonly string[] All = + { + Service, + Method, + }; } diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs index 380841da..8f6c80d2 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs @@ -1,101 +1,98 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Linq; -using Grpc.AspNetCore.Server; +using Grpc.AspNetCore.Server; +using Microsoft.AspNetCore.Http; -namespace Prometheus +namespace Prometheus; + +// Modeled after HttpRequestMiddlewareBase, just with gRPC specific functionality. +internal abstract class GrpcRequestMiddlewareBase + where TCollector : class, ICollector + where TChild : class, ICollectorChild { - // Modeled after HttpRequestMiddlewareBase, just with gRPC specific functionality. - internal abstract class GrpcRequestMiddlewareBase - where TCollector : class, ICollector - where TChild : class, ICollectorChild - { - /// - /// The set of labels from among the defaults that this metric supports. - /// - /// This set will be automatically extended with labels for additional - /// route parameters when creating the default metric instance. - /// - protected abstract string[] DefaultLabels { get; } + /// + /// The set of labels from among the defaults that this metric supports. + /// + /// This set will be automatically extended with labels for additional + /// route parameters when creating the default metric instance. + /// + protected abstract string[] DefaultLabels { get; } - /// - /// Creates the default metric instance with the specified set of labels. - /// Only used if the caller does not provide a custom metric instance in the options. - /// - protected abstract TCollector CreateMetricInstance(string[] labelNames); + /// + /// Creates the default metric instance with the specified set of labels. + /// Only used if the caller does not provide a custom metric instance in the options. + /// + protected abstract TCollector CreateMetricInstance(string[] labelNames); - /// - /// The factory to use for creating the default metric for this middleware. - /// Not used if a custom metric is alreaedy provided in options. - /// - protected MetricFactory MetricFactory { get; } + /// + /// The factory to use for creating the default metric for this middleware. + /// Not used if a custom metric is alreaedy provided in options. + /// + protected MetricFactory MetricFactory { get; } - private readonly TCollector _metric; + private readonly TCollector _metric; + + protected GrpcRequestMiddlewareBase(GrpcMetricsOptionsBase? options, TCollector? customMetric) + { + MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); - protected GrpcRequestMiddlewareBase(GrpcMetricsOptionsBase? options, TCollector? customMetric) + if (customMetric != null) { - MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); + _metric = customMetric; + ValidateNoUnexpectedLabelNames(); + } + else + { + _metric = CreateMetricInstance(DefaultLabels); + } + } - if (customMetric != null) - { - _metric = customMetric; - ValidateNoUnexpectedLabelNames(); - } - else - { - _metric = CreateMetricInstance(DefaultLabels); - } + protected TChild? CreateChild(HttpContext context) + { + var metadata = context.GetEndpoint()?.Metadata?.GetMetadata(); + if (metadata == null) + { + // Not a gRPC request + return null; } - protected TChild? CreateChild(HttpContext context) + if (!_metric.LabelNames.Any()) { - var metadata = context.GetEndpoint()?.Metadata?.GetMetadata(); - if (metadata == null) - { - // Not a gRPC request - return null; - } + return _metric.Unlabelled; + } - if (!_metric.LabelNames.Any()) - { - return _metric.Unlabelled; - } + return CreateChild(context, metadata); + } - return CreateChild(context, metadata); - } + protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata) + { + var labelValues = new string[_metric.LabelNames.Length]; - protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata) + for (var i = 0; i < labelValues.Length; i++) { - var labelValues = new string[_metric.LabelNames.Length]; - - for (var i = 0; i < labelValues.Length; i++) + switch (_metric.LabelNames[i]) { - switch (_metric.LabelNames[i]) - { - case GrpcRequestLabelNames.Service: - labelValues[i] = metadata.Method.ServiceName; - break; - case GrpcRequestLabelNames.Method: - labelValues[i] = metadata.Method.Name; - break; - default: - // Should never reach this point because we validate in ctor. - throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}"); - } + case GrpcRequestLabelNames.Service: + labelValues[i] = metadata.Method.ServiceName; + break; + case GrpcRequestLabelNames.Method: + labelValues[i] = metadata.Method.Name; + break; + default: + // Should never reach this point because we validate in ctor. + throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}"); } - - return _metric.WithLabels(labelValues); } - /// - /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. - /// - private void ValidateNoUnexpectedLabelNames() - { - var unexpected = _metric.LabelNames.Except(DefaultLabels); + return _metric.WithLabels(labelValues); + } - if (unexpected.Any()) - throw new ArgumentException($"Provided custom gRPC request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); - } + /// + /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. + /// + private void ValidateNoUnexpectedLabelNames() + { + var unexpected = _metric.LabelNames.Except(DefaultLabels); + + if (unexpected.Any()) + throw new ArgumentException($"Provided custom gRPC request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); } } \ No newline at end of file diff --git a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj index 056035a1..7d1556fa 100644 --- a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj +++ b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj @@ -1,38 +1,72 @@  - - netcoreapp3.1 - Prometheus - - false - - true - - ..\Resources\prometheus-net.snk - - preview - enable - - true - true - 1701;1702;1705;1591 - - - - bin\Release\netstandard2.0\Prometheus.Grpc.xml - - - - bin\Debug\netstandard2.0\Prometheus.Grpc.xml - - - - - - - 2.28.0 - - - + + net6.0 + Prometheus + + false + + true + ..\Resources\prometheus-net.snk + + enable + enable + True + True + 1591 + + latest + 9999 + + True + + + true + true + + + prometheus-net.AspNetCore.Grpc + sandersaares + prometheus-net + prometheus-net + ASP.NET Core gRPC integration with Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + metrics prometheus aspnetcore + MIT + True + snupkg + + + + + true + + + + + True + \ + + + True + \ + + + + + + + + 2.48.0 + + + + + + + diff --git a/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs b/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs index c8afbf92..187fa7aa 100644 --- a/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs +++ b/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs @@ -1,15 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Prometheus +namespace Prometheus; + +public static class HealthCheckBuilderExtensions { - public static class HealthCheckBuilderExtensions + public static IHealthChecksBuilder ForwardToPrometheus(this IHealthChecksBuilder builder, PrometheusHealthCheckPublisherOptions? options = null) { - public static IHealthChecksBuilder ForwardToPrometheus(this IHealthChecksBuilder builder, PrometheusHealthCheckPublisherOptions? options = null) - { - builder.Services.AddSingleton(provider => new PrometheusHealthCheckPublisher(options)); + builder.Services.AddSingleton(provider => new PrometheusHealthCheckPublisher(options)); - return builder; - } + return builder; } } diff --git a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj index 4c175992..68ade9f2 100644 --- a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj +++ b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj @@ -1,51 +1,74 @@  - netstandard2.0;netcoreapp3.1 + net6.0 Prometheus false true - ..\Resources\prometheus-net.snk - preview + enable enable - - true - + True + True + 1591 + + latest + 9999 + + True + + + true + true - - bin\Release\netstandard2.0\Prometheus.AspNetCore.HealthChecks.xml - true - 1701;1702;1705;1591 + + prometheus-net.AspNetCore.HealthChecks + sandersaares + prometheus-net + prometheus-net + ASP.NET Core Health Checks integration with Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + metrics prometheus aspnetcore + MIT + True + snupkg - - bin\Debug\netstandard2.0\Prometheus.AspNetCore.HealthChecks.xml - true - 1701;1702;1705;1591 + + + true - - - - - + + True + \ + + + True + \ + - - + + - + + - + + + diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs index 0a8b03f4..95b08115 100644 --- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs +++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs @@ -1,43 +1,39 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; -using System; -using System.Threading; -using System.Threading.Tasks; -namespace Prometheus +namespace Prometheus; + +/// +/// Publishes ASP.NET Core Health Check statuses as Prometheus metrics. +/// +internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher { - /// - /// Publishes ASP.NET Core Health Check statuses as Prometheus metrics. - /// - internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher - { - private readonly Gauge _checkStatus; + private readonly Gauge _checkStatus; - public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options) - { - _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().Gauge; - } + public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options) + { + _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().GetDefaultGauge(); + } - public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) - { - foreach (var reportEntry in report.Entries) - _checkStatus.WithLabels(reportEntry.Key).Set(HealthStatusToMetricValue(reportEntry.Value.Status)); + public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + foreach (var reportEntry in report.Entries) + _checkStatus.WithLabels(reportEntry.Key).Set(HealthStatusToMetricValue(reportEntry.Value.Status)); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private static double HealthStatusToMetricValue(HealthStatus status) + private static double HealthStatusToMetricValue(HealthStatus status) + { + switch (status) { - switch (status) - { - case HealthStatus.Unhealthy: - return 0; - case HealthStatus.Degraded: - return 0.5; - case HealthStatus.Healthy: - return 1; - default: - throw new NotSupportedException($"Unexpected HealthStatus value: {status}"); - } + case HealthStatus.Unhealthy: + return 0; + case HealthStatus.Degraded: + return 0.5; + case HealthStatus.Healthy: + return 1; + default: + throw new NotSupportedException($"Unexpected HealthStatus value: {status}"); } } } diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs index bb4cd606..df2c5400 100644 --- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs +++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs @@ -1,15 +1,14 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class PrometheusHealthCheckPublisherOptions { - public sealed class PrometheusHealthCheckPublisherOptions - { - private const string DefaultName = "aspnetcore_healthcheck_status"; - private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)"; + private const string DefaultName = "aspnetcore_healthcheck_status"; + private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)"; - public Gauge Gauge { get; set; } = - Metrics.CreateGauge(DefaultName, DefaultHelp, new GaugeConfiguration - { - LabelNames = new string[] { "name" }, - SuppressInitialValue = true - }); + public Gauge? Gauge { get; set; } + + public Gauge GetDefaultGauge() + { + return Metrics.CreateGauge(DefaultName, DefaultHelp, labelNames: new[] { "name" }); } } diff --git a/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs index 6e82e87d..af2416ff 100644 --- a/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs @@ -1,47 +1,45 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using System.Threading.Tasks; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// If routing data is available before executing the inner handler, this routing data is captured +/// and can be used later by other middlewares that wish not to be affected by runtime changes to routing data. +/// +/// +/// This is intended to be executed after the .UseRouting() middleware that performs ASP.NET Core 3 endpoint routing. +/// +/// The captured route data is stored in the context via ICapturedRouteDataFeature. +/// +internal sealed class CaptureRouteDataMiddleware { - /// - /// If routing data is available before executing the inner handler, this routing data is captured - /// and can be used later by other middlewares that wish not to be affected by runtime changes to routing data. - /// - /// - /// This is intended to be executed after the .UseRouting() middleware that performs ASP.NET Core 3 endpoint routing. - /// - /// The captured route data is stored in the context via ICapturedRouteDataFeature. - /// - internal sealed class CaptureRouteDataMiddleware - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public CaptureRouteDataMiddleware(RequestDelegate next) - { - _next = next; - } + public CaptureRouteDataMiddleware(RequestDelegate next) + { + _next = next; + } - public Task Invoke(HttpContext context) - { - TryCaptureRouteData(context); + public Task Invoke(HttpContext context) + { + TryCaptureRouteData(context); - return _next(context); - } + return _next(context); + } - private static void TryCaptureRouteData(HttpContext context) - { - var routeData = context.GetRouteData(); + private static void TryCaptureRouteData(HttpContext context) + { + var routeData = context.GetRouteData(); - if (routeData == null || routeData.Values.Count <= 0) - return; + if (routeData == null || routeData.Values.Count <= 0) + return; - var capturedRouteData = new CapturedRouteDataFeature(); + var capturedRouteData = new CapturedRouteDataFeature(); - foreach (var pair in routeData.Values) - capturedRouteData.Values.Add(pair.Key, pair.Value); + foreach (var pair in routeData.Values) + capturedRouteData.Values.Add(pair.Key, pair.Value); - context.Features.Set(capturedRouteData); - } + context.Features.Set(capturedRouteData); } } diff --git a/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs b/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs index 847ff518..f2562e2b 100644 --- a/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs +++ b/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Routing; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +sealed class CapturedRouteDataFeature : ICapturedRouteDataFeature { - sealed class CapturedRouteDataFeature : ICapturedRouteDataFeature - { - public RouteValueDictionary Values { get; } = new RouteValueDictionary(); - } + public RouteValueDictionary Values { get; } = new RouteValueDictionary(); } diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs b/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs new file mode 100644 index 00000000..830781c9 --- /dev/null +++ b/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; + +namespace Prometheus.HttpMetrics; + +public sealed class HttpCustomLabel +{ + /// + /// Name of the Prometheus label. + /// + public string LabelName { get; } + + /// + /// A method that extracts the label value from the HttpContext of the request being handled. + /// + public Func LabelValueProvider { get; } + + public HttpCustomLabel(string labelName, Func labelValueProvider) + { + LabelName = labelName; + LabelValueProvider = labelValueProvider; + } +} diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs index b8e07779..bff49072 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs @@ -1,38 +1,30 @@ using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +internal sealed class HttpInProgressMiddleware : HttpRequestMiddlewareBase, IGauge> { - internal sealed class HttpInProgressMiddleware : HttpRequestMiddlewareBase, IGauge> - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public HttpInProgressMiddleware(RequestDelegate next, HttpInProgressOptions? options) - : base(options, options?.Gauge) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } + public HttpInProgressMiddleware(RequestDelegate next, HttpInProgressOptions options) + : base(options, options?.Gauge) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } - public async Task Invoke(HttpContext context) + public async Task Invoke(HttpContext context) + { + // CreateChild() will take care of applying the right labels, no need to think hard about it here. + using (CreateChild(context).TrackInProgress()) { - // In ASP.NET Core 2, we will not have route data, so we cannot record controller/action labels. - // In ASP.NET Core 3, we will have this data and can record the labels. - // CreateChild() will take care of applying the right labels, no need to think hard about it here. - using (CreateChild(context).TrackInProgress()) - { - await _next(context); - } + await _next(context); } + } - protected override string[] DefaultLabels => HttpRequestLabelNames.PotentiallyAvailableBeforeExecutingFinalHandler; + protected override string[] BaselineLabels => HttpRequestLabelNames.DefaultsAvailableBeforeExecutingFinalHandler; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( - "http_requests_in_progress", - "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.", - new GaugeConfiguration - { - LabelNames = labelNames - }); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( + "http_requests_in_progress", + "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.", + labelNames); } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs index 7e3d30b2..df09ea3d 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpInProgressOptions : HttpMetricsOptionsBase { - public sealed class HttpInProgressOptions : HttpMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Gauge { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Gauge { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs index 3d5af486..ab4ca5d7 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs @@ -1,25 +1,66 @@ -using System.Collections.Generic; +namespace Prometheus.HttpMetrics; -namespace Prometheus.HttpMetrics +public abstract class HttpMetricsOptionsBase { - public abstract class HttpMetricsOptionsBase - { - public bool Enabled { get; set; } = true; - - /// - /// Additional route parameters to include beyond the defaults (controller/action). - /// This may be useful if you have, for example, a "version" parameter for API versioning. - /// - /// - /// Metric labels are automatically added for these parameters, unless you provide your - /// own metric instance in the options (in which case you must add the required labels). - /// - public List AdditionalRouteParameters { get; set; } = new List(); - - /// - /// Allows you to override the registry used to create the default metric instance. - /// Value is ignored if you specify a custom metric instance in the options. - /// - public CollectorRegistry? Registry { get; set; } - } + public bool Enabled { get; set; } = true; + + /// + /// Transforms the label value from it's raw value (e.g. 200, 404) into a compressed + /// alternative (e.g. 2xx, 4xx). Setting this to true can be used to reduce the cardinality of metrics produced while still clearly communicating + /// success and error conditions (client vs server error). Defaults to false. + /// + public bool ReduceStatusCodeCardinality { get; set; } = false; + + /// + /// Additional route parameters to include beyond the defaults (controller/action). + /// This may be useful if you have, for example, a "version" parameter for API versioning. + /// + /// + /// Metric labels are automatically defined for these parameters, unless you provide your + /// own metric instance in the options (in which case you must add the required labels). + /// + public List AdditionalRouteParameters { get; set; } = new List(); + + /// + /// Additional custom labels to add to the metrics, with values extracted from the HttpContext of incoming requests. + /// + /// + /// Metric labels are automatically defined for these, unless you provide your + /// own metric instance in the options (in which case you must add the required labels). + /// + public List CustomLabels { get; set; } = new List(); + + /// + /// Allows you to override the registry used to create the default metric instance. + /// + /// + /// Value is ignored if you specify a custom metric instance or metric factory in the options (instance overrides factory overrides registry). + /// + public CollectorRegistry? Registry { get; set; } + + /// + /// Allows you to override the metric factory used to create the default metric instance. + /// + /// + /// Value is ignored if you specify a custom metric instance in the options (instance overrides factory overrides registry). + /// + public IMetricFactory? MetricFactory { get; set; } + + /// + /// Enables custom logic to determine whether an exemplar should be recorded for a specific HTTP request. + /// This will be called after request processing has completed and the response has been filled by inner handlers. + /// + /// + /// By default, we always record an exemplar (if an exemplar is available for the given request/response pair). + /// + public HttpRequestExemplarPredicate ExemplarPredicate { get; set; } = _ => true; + + /// + /// If set, the "page" label will be considered one of the built-in default labels. + /// This is only enabled if Razor Pages is detected at the middleware setup stage. + /// + /// The value is ignored if a custom metric is provided (though the user may still add + /// the "page" label themselves via AdditionalRouteParameters and it will work). + /// + internal bool IncludePageLabelInDefaultsInternal { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs index ffebc8dd..f133f8d0 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs @@ -1,27 +1,85 @@ -namespace Prometheus.HttpMetrics +using Microsoft.AspNetCore.Http; + +namespace Prometheus.HttpMetrics; + +public sealed class HttpMiddlewareExporterOptions { - public sealed class HttpMiddlewareExporterOptions + public HttpInProgressOptions InProgress { get; set; } = new HttpInProgressOptions(); + public HttpRequestCountOptions RequestCount { get; set; } = new HttpRequestCountOptions(); + public HttpRequestDurationOptions RequestDuration { get; set; } = new HttpRequestDurationOptions(); + + /// + /// Whether to capture metrics for queries to the /metrics endpoint (where metrics are exported by default). Defaults to false. + /// This matches against URLs starting with the /metrics string specifically - if you use a custom metrics endpoint, this will not match. + /// + public bool CaptureMetricsUrl { get; set; } + + /// + /// Configures all the different types of metrics to use reduced status code cardinality (using 2xx instead of 200, 201 etc). + /// + public void ReduceStatusCodeCardinality() + { + InProgress.ReduceStatusCodeCardinality = true; + RequestCount.ReduceStatusCodeCardinality = true; + RequestDuration.ReduceStatusCodeCardinality = true; + } + + /// + /// Adds an additional route parameter to all the HTTP metrics. + /// + /// Helper method to avoid manually adding it to each one. + /// + public void AddRouteParameter(HttpRouteParameterMapping mapping) + { + InProgress.AdditionalRouteParameters.Add(mapping); + RequestCount.AdditionalRouteParameters.Add(mapping); + RequestDuration.AdditionalRouteParameters.Add(mapping); + } + + /// + /// Adds a custom label to all the HTTP metrics. + /// + /// Helper method to avoid manually adding it to each one. + /// + public void AddCustomLabel(HttpCustomLabel mapping) + { + InProgress.CustomLabels.Add(mapping); + RequestCount.CustomLabels.Add(mapping); + RequestDuration.CustomLabels.Add(mapping); + } + + /// + /// Adds a custom label to all the HTTP metrics. + /// + /// Helper method to avoid manually adding it to each one. + /// + public void AddCustomLabel(string labelName, Func valueProvider) + { + var mapping = new HttpCustomLabel(labelName, valueProvider); + + InProgress.CustomLabels.Add(mapping); + RequestCount.CustomLabels.Add(mapping); + RequestDuration.CustomLabels.Add(mapping); + } + + /// + /// Configures the middleware to use a custom metric factory for creating the metrics. + /// This provides an easy way to add custom static labels and configure exemplar behavior for all created metrics. + /// + public void SetMetricFactory(IMetricFactory metricFactory) + { + InProgress.MetricFactory = metricFactory; + RequestCount.MetricFactory = metricFactory; + RequestDuration.MetricFactory = metricFactory; + } + + /// + /// Configures the options that are shared between all metrics exposed by the HTTP server exporter. + /// + public void ConfigureMeasurements(Action configure) { - public HttpInProgressOptions InProgress { get; set; } = new HttpInProgressOptions(); - public HttpRequestCountOptions RequestCount { get; set; } = new HttpRequestCountOptions(); - public HttpRequestDurationOptions RequestDuration { get; set; } = new HttpRequestDurationOptions(); - - /// - /// Whether to capture metrics for queries to the /metrics endpoint (where metrics are exported by default). Defaults to false. - /// This matches against URLs starting with the /metrics string specifically - if you use a custom metrics endpoint, this will not match. - /// - public bool CaptureMetricsUrl { get; set; } - - /// - /// Adds an additional route parameter to all the HTTP metrics. - /// - /// Helper method to avoid manually adding it to each one. - /// - public void AddRouteParameter(HttpRouteParameterMapping mapping) - { - InProgress.AdditionalRouteParameters.Add(mapping); - RequestCount.AdditionalRouteParameters.Add(mapping); - RequestDuration.AdditionalRouteParameters.Add(mapping); - } + configure(InProgress); + configure(RequestCount); + configure(RequestDuration); } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs index fae782ff..faba435e 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs @@ -1,42 +1,38 @@ using Microsoft.AspNetCore.Http; -using System; -using System.Threading.Tasks; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +internal sealed class HttpRequestCountMiddleware : HttpRequestMiddlewareBase, ICounter> { - internal sealed class HttpRequestCountMiddleware : HttpRequestMiddlewareBase, ICounter> + private readonly RequestDelegate _next; + private readonly HttpRequestCountOptions _options; + + public HttpRequestCountMiddleware(RequestDelegate next, HttpRequestCountOptions options) + : base(options, options?.Counter) { - private readonly RequestDelegate _next; + _next = next ?? throw new ArgumentNullException(nameof(next)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } - public HttpRequestCountMiddleware(RequestDelegate next, HttpRequestCountOptions? options) - : base(options, options?.Counter) + public async Task Invoke(HttpContext context) + { + try { - _next = next ?? throw new ArgumentNullException(nameof(next)); + await _next(context); } - - public async Task Invoke(HttpContext context) + finally { - try - { - await _next(context); - } - finally - { - // We need to record metrics after inner handler execution because routing data in - // ASP.NET Core 2 is only available *after* executing the next request delegate. - // So we would not have the right labels if we tried to create the child early on. - CreateChild(context).Inc(); - } + // We pass either null (== use default exemplar provider) or None (== do not record exemplar). + Exemplar? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; + + CreateChild(context).Inc(exemplar); } + } - protected override string[] DefaultLabels => HttpRequestLabelNames.All; + protected override string[] BaselineLabels => HttpRequestLabelNames.Default; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( - "http_requests_received_total", - "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.", - new CounterConfiguration - { - LabelNames = labelNames - }); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( + "http_requests_received_total", + "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.", + labelNames); } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs index d395cea2..e09e0d98 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpRequestCountOptions : HttpMetricsOptionsBase { - public sealed class HttpRequestCountOptions : HttpMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Counter { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Counter { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs index 226d0590..820f8da4 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs @@ -1,47 +1,45 @@ using Microsoft.AspNetCore.Http; -using System; -using System.Diagnostics; -using System.Threading.Tasks; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +internal sealed class HttpRequestDurationMiddleware : HttpRequestMiddlewareBase, IHistogram> { - internal sealed class HttpRequestDurationMiddleware : HttpRequestMiddlewareBase, IHistogram> + private readonly RequestDelegate _next; + private readonly HttpRequestDurationOptions _options; + + public HttpRequestDurationMiddleware(RequestDelegate next, HttpRequestDurationOptions options) + : base(options, options?.Histogram) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task Invoke(HttpContext context) { - private readonly RequestDelegate _next; + var stopWatch = ValueStopwatch.StartNew(); - public HttpRequestDurationMiddleware(RequestDelegate next, HttpRequestDurationOptions? options) - : base(options, options?.Histogram) + try { - _next = next ?? throw new ArgumentNullException(nameof(next)); + await _next(context); } - - public async Task Invoke(HttpContext context) + finally { - var stopWatch = ValueStopwatch.StartNew(); - - // We need to write this out in long form instead of using a timer because routing data in - // ASP.NET Core 2 is only available *after* executing the next request delegate. - // So we would not have the right labels if we tried to create the child early on. - try - { - await _next(context); - } - finally - { - CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds); - } + // We pass either null (== use default exemplar provider) or None (== do not record exemplar). + Exemplar? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; + + CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds, exemplar); } + } - protected override string[] DefaultLabels => HttpRequestLabelNames.All; + protected override string[] BaselineLabels => HttpRequestLabelNames.Default; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( - "http_request_duration_seconds", - "The duration of HTTP requests processed by an ASP.NET Core application.", - new HistogramConfiguration - { - // 1 ms to 32K ms buckets - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), - LabelNames = labelNames - }); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( + "http_request_duration_seconds", + "The duration of HTTP requests processed by an ASP.NET Core application.", + labelNames, + new HistogramConfiguration + { + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), + }); } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs index ccdd1272..ddda75aa 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpRequestDurationOptions : HttpMetricsOptionsBase { - public sealed class HttpRequestDurationOptions : HttpMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Histogram { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Histogram { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs new file mode 100644 index 00000000..91fce1f9 --- /dev/null +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Http; + +namespace Prometheus.HttpMetrics; + +public delegate bool HttpRequestExemplarPredicate(HttpContext context); diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs index ff4b21be..d2fbcfdd 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs @@ -1,37 +1,54 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// Label names used by the HTTP request handler metrics system. +/// +public static class HttpRequestLabelNames { - /// - /// Label names reserved for the use by the HTTP request metrics. - /// - public static class HttpRequestLabelNames + public const string Code = "code"; + public const string Method = "method"; + public const string Controller = "controller"; + public const string Action = "action"; + + // Not reserved for background-compatibility, as it used to be optional and user-supplied. + // Conditionally, it may also be automatically added to metrics. + public const string Page = "page"; + + // Not reserved for background-compatibility, as it used to be optional and user-supplied. + public const string Endpoint = "endpoint"; + + // All labels that are supported by prometheus-net default logic. + // Useful if you want to define a custom metric that extends the default logic, without hardcoding the built-in label names. + public static readonly string[] All = { - public const string Code = "code"; - public const string Method = "method"; - public const string Controller = "controller"; - public const string Action = "action"; + Code, + Method, + Controller, + Action, + Page, + Endpoint + }; - public static readonly string[] All = - { - Code, - Method, - Controller, - Action - }; + // These are reserved and may only be used with the default logic. + internal static readonly string[] Default = + { + Code, + Method, + Controller, + Action + }; - internal static readonly string[] PotentiallyAvailableBeforeExecutingFinalHandler = - { - // Always available, part of request. - Method, - // These two are available only in ASP.NET Core 3. - Controller, - Action - }; + internal static readonly string[] DefaultsAvailableBeforeExecutingFinalHandler = + { + Method, + Controller, + Action + }; - // Labels that do not need routing information to be collected. - internal static readonly string[] NonRouteSpecific = - { - Code, - Method - }; - } + // Labels that do not need routing information to be collected. + internal static readonly string[] NonRouteSpecific = + { + Code, + Method + }; } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs index 108ae561..0a5cea70 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs @@ -1,206 +1,329 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// This base class performs the data management necessary to associate the correct labels and values +/// with HTTP request metrics, depending on the options the user has provided for the HTTP metric middleware. +/// +/// The following labels are supported: +/// 'code' (HTTP status code) +/// 'method' (HTTP request method) +/// 'controller' (The Controller used to fulfill the HTTP request) +/// 'action' (The Action used to fulfill the HTTP request) +/// Any other label - from one of: +/// * HTTP route parameter (if name/mapping specified in options; name need not match). +/// * custom logic (callback decides value for each request) +/// +/// The 'code' and 'method' data are taken from the current HTTP context. +/// 'controller', 'action' and route parameter labels will be taken from the request routing information. +/// +/// If a custom metric is provided in the options, it must not be missing any labels for explicitly defined +/// custom route parameters. However, it is permitted to lack any of the default labels (code/method/...). +/// +internal abstract class HttpRequestMiddlewareBase + where TCollector : class, ICollector + where TChild : class, ICollectorChild { /// - /// This base class performs the data management necessary to associate the correct labels and values - /// with HTTP request metrics, depending on the options the user has provided for the HTTP metric middleware. + /// The set of labels from among the defaults that this metric supports. /// - /// The following labels are supported: - /// 'code' (HTTP status code) - /// 'method' (HTTP request method) - /// 'controller' (The Controller used to fulfill the HTTP request) - /// 'action' (The Action used to fulfill the HTTP request) - /// Any other label - custom HTTP route parameter (if specified in options). + /// This set will be automatically extended with labels for additional + /// route parameters and custom labels when creating the default metric instance. /// - /// The 'code' and 'method' data are taken from the current HTTP context. - /// Other labels will be taken from the request routing information. - /// - /// If a custom metric is provided in the options, it must not be missing any labels for explicitly defined - /// custom route parameters. However, it is permitted to lack any of the default labels (code/method/...). + /// It will also be extended by additional built-in logic (page, endpoint). + /// + protected abstract string[] BaselineLabels { get; } + + /// + /// Creates the default metric instance with the specified set of labels. + /// Only used if the caller does not provide a custom metric instance in the options. /// - internal abstract class HttpRequestMiddlewareBase - where TCollector : class, ICollector - where TChild : class, ICollectorChild + protected abstract TCollector CreateMetricInstance(string[] labelNames); + + /// + /// The factory to use for creating the default metric for this middleware. + /// Not used if a custom metric is already provided in options. + /// + protected IMetricFactory MetricFactory { get; } + + private readonly List _additionalRouteParameters; + private readonly List _customLabels; + private readonly TCollector _metric; + + // For labels that are route parameter mappings. + private readonly Dictionary _labelToRouteParameterMap; + + // For labels that use a custom value provider. + private readonly Dictionary> _labelToValueProviderMap; + + private readonly bool _labelsRequireRouteData; + private readonly bool _reduceStatusCodeCardinality; + + protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase options, TCollector? customMetric) { - /// - /// The set of labels from among the defaults that this metric supports. - /// - /// This set will be automatically extended with labels for additional - /// route parameters when creating the default metric instance. - /// - protected abstract string[] DefaultLabels { get; } - - /// - /// Creates the default metric instance with the specified set of labels. - /// Only used if the caller does not provide a custom metric instance in the options. - /// - protected abstract TCollector CreateMetricInstance(string[] labelNames); - - /// - /// The factory to use for creating the default metric for this middleware. - /// Not used if a custom metric is already provided in options. - /// - protected MetricFactory MetricFactory { get; } - - private readonly ICollection _additionalRouteParameters; - private readonly TCollector _metric; - - private readonly Dictionary _labelToRouteParameterMap; - - private readonly bool _labelsRequireRouteData; - - protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase? options, TCollector? customMetric) - { - MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); + MetricFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(options.Registry ?? Metrics.DefaultRegistry); - _additionalRouteParameters = options?.AdditionalRouteParameters ?? new List(0); + _additionalRouteParameters = options.AdditionalRouteParameters ?? new List(0); + _customLabels = options.CustomLabels ?? new List(0); - ValidateAdditionalRouteParameterSet(); - _labelToRouteParameterMap = CreateLabelToRouteParameterMap(); + if (options.IncludePageLabelInDefaultsInternal) + AddPageLabelIfNoConflict(customMetric); - if (customMetric != null) - { - _metric = customMetric; + AddEndpointLabelIfNoConflict(customMetric); - ValidateNoUnexpectedLabelNames(); - ValidateAdditionalRouteParametersPresentInMetricLabelNames(); - } - else - { - _metric = CreateMetricInstance(CreateDefaultLabelSet()); - } + ValidateMappings(); + _labelToRouteParameterMap = CreateLabelToRouteParameterMap(); + _reduceStatusCodeCardinality = options?.ReduceStatusCodeCardinality ?? false; + _labelToValueProviderMap = CreateLabelToValueProviderMap(); - _labelsRequireRouteData = _metric.LabelNames.Except(HttpRequestLabelNames.NonRouteSpecific).Any(); - } + if (customMetric != null) + { + _metric = customMetric; - /// - /// Creates the metric child instance to use for measurements. - /// - /// - /// Internal for testing purposes. - /// - protected internal TChild CreateChild(HttpContext context) + ValidateNoUnexpectedLabelNames(); + ValidateAdditionalRouteParametersPresentInMetricLabelNames(); + ValidateCustomLabelsPresentInMetricLabelNames(); + } + else { - if (!_metric.LabelNames.Any()) - return _metric.Unlabelled; + _metric = CreateMetricInstance(CreateDefaultLabelSet()); + } - if (!_labelsRequireRouteData) - return CreateChild(context, null); + _labelsRequireRouteData = _metric.LabelNames.Except(HttpRequestLabelNames.NonRouteSpecific).Any(); + } - var routeData = context.Features.Get()?.Values; + private void AddPageLabelIfNoConflict(TCollector? customMetric) + { + // We were asked to add the "page" label because Razor Pages was detected. + // We will only do this if nothing else has already occupied the "page" label. + // If a custom metric is used, we also skip this if it has no "page" label name defined. + // + // The possible conflicts are: + // * an existing route parameter mapping (which works out the same as our logic, so fine) + // * custom logic that defines a "page" label (in which case we allow it to win, for backward compatibility). - // If we have captured route data, we always prefer it. - // Otherwise, we extract new route data right now. - if (routeData == null) - routeData = context.GetRouteData()?.Values; + if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Page)) + return; - return CreateChild(context, routeData); - } + if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Page)) + return; + + if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Page)) + return; - protected TChild CreateChild(HttpContext context, RouteValueDictionary? routeData) + // If we got so far, we are good - all preconditions for adding "page" label exist. + _additionalRouteParameters.Add(new HttpRouteParameterMapping("page")); + } + + private void AddEndpointLabelIfNoConflict(TCollector? customMetric) + { + // We always try to add an "endpoint" label with the endpoint routing route pattern. + // We will only do this if nothing else has already occupied the "endpoint" label. + // If a custom metric is used, we also skip this if it has no "endpoint" label name defined. + // + // The possible conflicts are: + // * an existing route parameter mapping + // * custom logic that defines an "endpoint" label + // + // In case of conflict, we let the user-defined item win. + + if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint)) + return; + + if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint)) + return; + + if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Endpoint)) + return; + + _customLabels.Add(new HttpCustomLabel(HttpRequestLabelNames.Endpoint, context => { - var labelValues = new string[_metric.LabelNames.Length]; + var endpoint = context.GetEndpoint() as RouteEndpoint; + return endpoint?.RoutePattern.RawText ?? ""; + })); + } + + /// + /// Creates the metric child instance to use for measurements. + /// + /// + /// Internal for testing purposes. + /// + protected internal TChild CreateChild(HttpContext context) + { + if (!_metric.LabelNames.Any()) + return _metric.Unlabelled; - for (var i = 0; i < labelValues.Length; i++) + if (!_labelsRequireRouteData) + return CreateChild(context, null); + + var routeData = context.Features.Get()?.Values; + + // If we have captured route data, we always prefer it. + // Otherwise, we extract new route data right now. + if (routeData == null) + routeData = context.GetRouteData()?.Values; + + return CreateChild(context, routeData); + } + + protected TChild CreateChild(HttpContext context, RouteValueDictionary? routeData) + { + var labelValues = new string[_metric.LabelNames.Length]; + + for (var i = 0; i < labelValues.Length; i++) + { + switch (_metric.LabelNames[i]) { - switch (_metric.LabelNames[i]) - { - case HttpRequestLabelNames.Method: - labelValues[i] = context.Request.Method; - break; - case HttpRequestLabelNames.Code: - labelValues[i] = context.Response.StatusCode.ToString(CultureInfo.InvariantCulture); - break; - default: - // We validate the label set on initialization, so it must be a route parameter if we get to this point. - var parameterName = _labelToRouteParameterMap[_metric.LabelNames[i]]; + case HttpRequestLabelNames.Method: + labelValues[i] = context.Request.Method; + break; + case HttpRequestLabelNames.Code: + labelValues[i] = _reduceStatusCodeCardinality ? Math.Floor(context.Response.StatusCode / 100.0).ToString("#xx") : context.Response.StatusCode.ToString(CultureInfo.InvariantCulture); + break; + default: + // If we get to this point it must be either: + if (_labelToRouteParameterMap.TryGetValue(_metric.LabelNames[i], out var parameterName)) + { + // A mapped route parameter. labelValues[i] = routeData?[parameterName] as string ?? string.Empty; - break; - } + } + else if (_labelToValueProviderMap.TryGetValue(_metric.LabelNames[i], out var valueProvider)) + { + // A custom label value provider. + labelValues[i] = valueProvider(context) ?? string.Empty; + } + else + { + // Something we do not have data for. + // This can happen if, for example, a custom metric inherits "all" the labels without reimplementing the "when do we add which label" + // logic that prometheus-net implements (which is an entirely reasonable design). So it might just add a "page" label when we have no + // page information. Instead of rejecting such custom metrics, we just leave the label value empty and carry on. + labelValues[i] = ""; + } + break; } - - return _metric.WithLabels(labelValues); } + return _metric.WithLabels(labelValues); + } - /// - /// Creates the full set of labels supported for the current metric. - /// - /// This merges (in unspecified order) the defaults from prometheus-net with any in options.AdditionalRouteParameters. - /// - private string[] CreateDefaultLabelSet() - { - return DefaultLabels.Concat(_additionalRouteParameters.Select(x => x.LabelName)).ToArray(); - } - private void ValidateAdditionalRouteParameterSet() - { - var parameterNames = _additionalRouteParameters.Select(x => x.ParameterName).ToList(); + /// + /// Creates the set of labels defined on the automatically created metric. + /// + private string[] CreateDefaultLabelSet() + { + return BaselineLabels + .Concat(_additionalRouteParameters.Select(x => x.LabelName)) + .Concat(_customLabels.Select(x => x.LabelName)) + .ToArray(); + } - if (parameterNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != parameterNames.Count) - throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same parameter name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters)); + /// + /// Creates the full set of labels ALLOWED for the current metric. + /// This may be a greater set than the labels automatically added to the default metric. + /// + private string[] CreateAllowedLabelSet() + { + return HttpRequestLabelNames.All + .Concat(_additionalRouteParameters.Select(x => x.LabelName)) + .Concat(_customLabels.Select(x => x.LabelName)) + .Distinct() // Some builtins may also exist in the customs, with customs overwriting. That's fine. + .ToArray(); + } - var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); + private void ValidateMappings() + { + var routeParameterLabelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); - if (labelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != labelNames.Count) - throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters)); + if (routeParameterLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != routeParameterLabelNames.Count) + throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters)); - if (HttpRequestLabelNames.All.Except(labelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.All.Length) - throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.All)}"); + if (HttpRequestLabelNames.Default.Except(routeParameterLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length) + throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}"); - var reservedParameterNames = new[] { "action", "controller" }; + var customLabelNames = _customLabels.Select(x => x.LabelName).ToList(); - if (reservedParameterNames.Except(parameterNames, StringComparer.InvariantCultureIgnoreCase).Count() != reservedParameterNames.Length) - throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved route parameter name. Reserved route parameter names are: {string.Join(", ", reservedParameterNames)}"); - } + if (customLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != customLabelNames.Count) + throw new ArgumentException("The set of custom labels contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.CustomLabels)); - private Dictionary CreateLabelToRouteParameterMap() - { - var map = new Dictionary(_additionalRouteParameters.Count + 2); + if (HttpRequestLabelNames.Default.Except(customLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length) + throw new ArgumentException($"The set of custom labels contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}"); - // Defaults are hardcoded. - map["action"] = "action"; - map["controller"] = "controller"; + if (customLabelNames.Intersect(routeParameterLabelNames).Any()) + throw new ArgumentException("The set of custom labels and the set of additional route parameters contain conflicting label names.", nameof(HttpMetricsOptionsBase.CustomLabels)); + } - // Any additional ones are merged. - foreach (var entry in _additionalRouteParameters) - map[entry.LabelName] = entry.ParameterName; + private Dictionary CreateLabelToRouteParameterMap() + { + var map = new Dictionary(_additionalRouteParameters.Count + 2); - return map; - } + // Defaults are hardcoded. + map["action"] = "action"; + map["controller"] = "controller"; - /// - /// Inspects the metric instance to ensure that all required labels are present. - /// - /// - /// If there are mappings to include route parameters in the labels, there must be labels defined for each such parameter. - /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. - /// - private void ValidateAdditionalRouteParametersPresentInMetricLabelNames() - { - var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); - var missing = labelNames.Except(_metric.LabelNames); + // Any additional ones are merged. + foreach (var entry in _additionalRouteParameters) + map[entry.LabelName] = entry.ParameterName; - if (missing.Any()) - throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); - } + return map; + } - /// - /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. - /// - private void ValidateNoUnexpectedLabelNames() - { - var allowedLabels = CreateDefaultLabelSet(); - var unexpected = _metric.LabelNames.Except(allowedLabels); + private Dictionary> CreateLabelToValueProviderMap() + { + var map = new Dictionary>(_customLabels.Count); - if (unexpected.Any()) - throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); - } + foreach (var entry in _customLabels) + map[entry.LabelName] = entry.LabelValueProvider; + + return map; + } + + /// + /// Inspects the metric instance to ensure that all required labels are present. + /// + /// + /// If there are mappings to include route parameters in the labels, there must be labels defined for each such parameter. + /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. + /// + private void ValidateAdditionalRouteParametersPresentInMetricLabelNames() + { + var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); + var missing = labelNames.Except(_metric.LabelNames); + + if (missing.Any()) + throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); + } + + /// + /// Inspects the metric instance to ensure that all required labels are present. + /// + /// + /// If there are mappings to include custom labels, there must be label names defined for each such parameter. + /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. + /// + private void ValidateCustomLabelsPresentInMetricLabelNames() + { + var labelNames = _customLabels.Select(x => x.LabelName).ToList(); + var missing = labelNames.Except(_metric.LabelNames); + + if (missing.Any()) + throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); + } + + /// + /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. + /// + private void ValidateNoUnexpectedLabelNames() + { + var allowedLabels = CreateAllowedLabelSet(); + var unexpected = _metric.LabelNames.Except(allowedLabels); + + if (unexpected.Any()) + throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); } } diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs index c1977984..fafab89a 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs @@ -1,40 +1,39 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// Maps an HTTP route parameter name to a Prometheus label name. +/// +/// +/// Typically, the parameter name and the label name will be equal. +/// The purpose of this is to enable capture of route parameters that conflict with built-in label names like "method" (HTTP method). +/// +public sealed class HttpRouteParameterMapping { /// - /// Maps an HTTP route parameter name to a Prometheus label name. + /// Name of the HTTP route parameter. /// - /// - /// Typically, the parameter name and the label name will be equal. - /// The purpose of this is to enable capture of route parameters that conflict with built-in label names like "method" (HTTP method). - /// - public sealed class HttpRouteParameterMapping - { - /// - /// Name of the HTTP route parameter. - /// - public string ParameterName { get; } - - /// - /// Name of the Prometheus label. - /// - public string LabelName { get; } + public string ParameterName { get; } - public HttpRouteParameterMapping(string name) - { - Collector.ValidateLabelName(name); + /// + /// Name of the Prometheus label. + /// + public string LabelName { get; } - ParameterName = name; - LabelName = name; - } + public HttpRouteParameterMapping(string name) + { + Collector.ValidateLabelName(name); - public HttpRouteParameterMapping(string parameterName, string labelName) - { - Collector.ValidateLabelName(labelName); + ParameterName = name; + LabelName = name; + } - ParameterName = parameterName; - LabelName = labelName; - } + public HttpRouteParameterMapping(string parameterName, string labelName) + { + Collector.ValidateLabelName(labelName); - public static implicit operator HttpRouteParameterMapping(string name) => new HttpRouteParameterMapping(name); + ParameterName = parameterName; + LabelName = labelName; } + + public static implicit operator HttpRouteParameterMapping(string name) => new HttpRouteParameterMapping(name); } diff --git a/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs b/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs index dc99e518..d712c9d3 100644 --- a/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs +++ b/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Routing; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +interface ICapturedRouteDataFeature { - interface ICapturedRouteDataFeature - { - RouteValueDictionary Values { get; } - } + RouteValueDictionary Values { get; } } diff --git a/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs b/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs index 32556a15..8f9987b2 100644 --- a/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs @@ -1,56 +1,67 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using Prometheus.HttpMetrics; -using System; -namespace Prometheus +namespace Prometheus; + +public static class HttpMetricsMiddlewareExtensions { - public static class HttpMetricsMiddlewareExtensions + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. + /// + /// Call this after .UseRouting(). + /// + public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, + Action configure) { - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. - /// - /// If using ASP.NET Core 3 or newer, call this after .UseRouting(). - /// - public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, - Action configure) - { - var options = new HttpMiddlewareExporterOptions(); + var options = new HttpMiddlewareExporterOptions(); + + configure?.Invoke(options); - configure?.Invoke(options); + app.UseHttpMetrics(options); - app.UseHttpMetrics(options); + return app; + } + + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. + /// + /// Call this after .UseRouting(). + /// + public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, + HttpMiddlewareExporterOptions? options = null) + { + options = options ?? new HttpMiddlewareExporterOptions(); - return app; + if (app.ApplicationServices.GetService() != null) + { + // If Razor Pages is enabled, we will automatically add a "page" route parameter to represent it. We do this only if no custom metric is used. + // If a custom metric is used, we still allow "page" label to be present and automatically fill it with the Razor Pages route parameter + // unless there is a custom label with this name added, in which case the custom label takes priority. + + options.InProgress.IncludePageLabelInDefaultsInternal = true; + options.RequestCount.IncludePageLabelInDefaultsInternal = true; + options.RequestDuration.IncludePageLabelInDefaultsInternal = true; } - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. - /// - /// If using ASP.NET Core 3 or newer, call this after .UseRouting(). - /// - public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, - HttpMiddlewareExporterOptions? options = null) + void ApplyConfiguration(IApplicationBuilder builder) { - options = options ?? new HttpMiddlewareExporterOptions(); - - void ApplyConfiguration(IApplicationBuilder builder) - { - builder.UseMiddleware(); - - if (options.InProgress.Enabled) - builder.UseMiddleware(options.InProgress); - if (options.RequestCount.Enabled) - builder.UseMiddleware(options.RequestCount); - if (options.RequestDuration.Enabled) - builder.UseMiddleware(options.RequestDuration); - } - - if (options.CaptureMetricsUrl) - ApplyConfiguration(app); - else - app.UseWhen(context => context.Request.Path != "/metrics", ApplyConfiguration); - - return app; + builder.UseMiddleware(); + + if (options.InProgress.Enabled) + builder.UseMiddleware(options.InProgress); + if (options.RequestCount.Enabled) + builder.UseMiddleware(options.RequestCount); + if (options.RequestDuration.Enabled) + builder.UseMiddleware(options.RequestDuration); } + + if (options.CaptureMetricsUrl) + ApplyConfiguration(app); + else + app.UseWhen(context => context.Request.Path != "/metrics", ApplyConfiguration); + + return app; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/KestrelMetricServer.cs b/Prometheus.AspNetCore/KestrelMetricServer.cs index 39b06b88..3dca354a 100644 --- a/Prometheus.AspNetCore/KestrelMetricServer.cs +++ b/Prometheus.AspNetCore/KestrelMetricServer.cs @@ -1,93 +1,114 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; -using System; using System.Net; using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -namespace Prometheus +namespace Prometheus; + +/// +/// A stand-alone Kestrel based metric server that saves you the effort of setting up the ASP.NET Core pipeline. +/// For all practical purposes, this is just a regular ASP.NET Core server that only serves Prometheus requests. +/// +public sealed class KestrelMetricServer : MetricHandler { - /// - /// A stand-alone Kestrel based metric server that saves you the effort of setting up the ASP.NET Core pipeline. - /// For all practical purposes, this is just a regular ASP.NET Core server that only serves Prometheus requests. - /// - public sealed class KestrelMetricServer : MetricHandler + public KestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate) + { + } + + public KestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this(LegacyOptions(hostname, port, url, registry, certificate)) { - public KestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate) + } + + private static KestrelMetricServerOptions LegacyOptions(string hostname, int port, string url, CollectorRegistry? registry, X509Certificate2? certificate) => + new KestrelMetricServerOptions { - } + Hostname = hostname, + Port = (ushort)port, + Url = url, + Registry = registry, + TlsCertificate = certificate, + }; + + public KestrelMetricServer(KestrelMetricServerOptions options) + { + _hostname = options.Hostname; + _port = options.Port; + _url = options.Url; + _certificate = options.TlsCertificate; - public KestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : base(registry) + // We use one callback to apply the legacy settings, and from within this we call the real callback. + _configureExporter = settings => { - _hostname = hostname; - _port = port; - _url = url; + // Legacy setting, may be overridden by ConfigureExporter. + settings.Registry = options.Registry; - _certificate = certificate; - } + if (options.ConfigureExporter != null) + options.ConfigureExporter(settings); + }; + } - private readonly string _hostname; - private readonly int _port; - private readonly string _url; + private readonly string _hostname; + private readonly int _port; + private readonly string _url; - private readonly X509Certificate2? _certificate; + private readonly X509Certificate2? _certificate; - protected override Task StartServer(CancellationToken cancel) - { - var s = _certificate != null ? "s" : ""; - var hostAddress = $"http{s}://{_hostname}:{_port}"; - - // If the caller needs to customize any of this, they can just set up their own web host and inject the middleware. - var builder = new WebHostBuilder() - .UseKestrel() - .UseIISIntegration() - .Configure(app => - { - // _registry will already be pre-configured by MetricHandler. - app.UseMetricServer(_url, _registry); - - // If there is any URL prefix, we just redirect people going to root URL to our prefix. - if (!string.IsNullOrWhiteSpace(_url.Trim('/'))) - { - app.MapWhen(context => context.Request.Path.Value.Trim('/') == "", - configuration => - { - configuration.Use((context, next) => - { - context.Response.Redirect(_url); - return Task.CompletedTask; - }); - }); - } - }); + private readonly Action _configureExporter; - if (_certificate != null) + protected override Task StartServer(CancellationToken cancel) + { + var s = _certificate != null ? "s" : ""; + var hostAddress = $"http{s}://{_hostname}:{_port}"; + + // If the caller needs to customize any of this, they can just set up their own web host and inject the middleware. + var builder = new WebHostBuilder() + .UseKestrel() + .UseIISIntegration() + .Configure(app => { - builder = builder.ConfigureServices(services => - { - Action configureEndpoint = options => - { - options.UseHttps(_certificate); - }; + app.UseMetricServer(_configureExporter, _url); - services.Configure(options => + // If there is any URL prefix, we just redirect people going to root URL to our prefix. + if (!string.IsNullOrWhiteSpace(_url.Trim('/'))) + { + app.MapWhen(context => context.Request.Path.Value?.Trim('/') == "", + configuration => { - options.Listen(IPAddress.Any, _port, configureEndpoint); + configuration.Use((HttpContext context, RequestDelegate next) => + { + context.Response.Redirect(_url); + return Task.CompletedTask; + }); }); - }); - } - else - { - builder = builder.UseUrls(hostAddress); - } + } + }); - var webHost = builder.Build(); - webHost.Start(); + if (_certificate != null) + { + builder = builder.ConfigureServices(services => + { + Action configureEndpoint = options => + { + options.UseHttps(_certificate); + }; - return webHost.WaitForShutdownAsync(cancel); + services.Configure(options => + { + options.Listen(IPAddress.Any, _port, configureEndpoint); + }); + }); } + else + { + builder = builder.UseUrls(hostAddress); + } + + var webHost = builder.Build(); + webHost.Start(); + + return webHost.WaitForShutdownAsync(cancel); } } diff --git a/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs b/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs new file mode 100644 index 00000000..b5215ca2 --- /dev/null +++ b/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Prometheus; + +public static class KestrelMetricServerExtensions +{ + public static IServiceCollection AddMetricServer(this IServiceCollection services, Action optionsCallback) + { + return services.AddHostedService(sp => + { + var options = new KestrelMetricServerOptions(); + optionsCallback(options); + return new MetricsExporterService(options); + }); + } + + private sealed class MetricsExporterService : BackgroundService + { + public MetricsExporterService(KestrelMetricServerOptions options) + { + _options = options; + } + + private readonly KestrelMetricServerOptions _options; + + protected override async Task ExecuteAsync(CancellationToken cancel) + { + using var metricServer = new KestrelMetricServer(_options); + metricServer.Start(); + + try + { + // Wait forever, until we are told to stop. + await Task.Delay(-1, cancel); + } + catch (OperationCanceledException) when (cancel.IsCancellationRequested) + { + // Time to stop. + } + } + } + +} diff --git a/Prometheus.AspNetCore/KestrelMetricServerOptions.cs b/Prometheus.AspNetCore/KestrelMetricServerOptions.cs new file mode 100644 index 00000000..817eebf5 --- /dev/null +++ b/Prometheus.AspNetCore/KestrelMetricServerOptions.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using System.Security.Cryptography.X509Certificates; + +namespace Prometheus; + +public sealed class KestrelMetricServerOptions +{ + /// + /// Will listen for requests using this hostname. "+" indicates listen on all hostnames. + /// By setting this to "localhost", you can easily prevent access from remote systems.- + /// + public string Hostname { get; set; } = "+"; + + public ushort Port { get; set; } + public string Url { get; set; } = "/metrics"; + public X509Certificate2? TlsCertificate { get; set; } + + // May be overridden by ConfigureExporter. + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it and prefer ConfigureExporter. + public CollectorRegistry? Registry { get; set; } + + /// + /// Allows metric exporter options to be configured in a flexible manner. + /// The callback is called after applying any values in KestrelMetricServerOptions. + /// + public Action? ConfigureExporter { get; set; } +} diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index b11fc079..e7ef2af5 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -1,62 +1,115 @@ using Microsoft.AspNetCore.Http; -using System; -using System.IO; -using System.Threading.Tasks; +using System.Net.Http.Headers; -namespace Prometheus +namespace Prometheus; + +/// +/// Prometheus metrics export middleware for ASP.NET Core. +/// +/// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly. +/// +public sealed class MetricServerMiddleware { - /// - /// Prometheus metrics export middleware for ASP.NET Core. - /// - /// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly. - /// - public sealed class MetricServerMiddleware + public MetricServerMiddleware(RequestDelegate next, Settings settings) + { + _registry = settings.Registry ?? Metrics.DefaultRegistry; + _enableOpenMetrics = settings.EnableOpenMetrics; + } + + public sealed class Settings + { + /// + /// Where do we take the metrics from. By default, we will take them from the global singleton registry. + /// + public CollectorRegistry? Registry { get; set; } + + /// + /// Whether we support the OpenMetrics exposition format. Required to publish exemplars. Defaults to enabled. + /// Use of OpenMetrics also requires that the client negotiate the OpenMetrics format via the HTTP "Accept" request header. + /// + public bool EnableOpenMetrics { get; set; } = true; + } + + private readonly CollectorRegistry _registry; + private readonly bool _enableOpenMetrics; + + private readonly record struct ProtocolNegotiationResult(ExpositionFormat ExpositionFormat, string ContentType); + + private static IEnumerable ExtractAcceptableMediaTypes(string acceptHeaderValue) { - public MetricServerMiddleware(RequestDelegate next, Settings settings) + var candidates = acceptHeaderValue.Split(','); + + foreach (var candidate in candidates) { - _registry = settings.Registry ?? Metrics.DefaultRegistry; + // It is conceivably possible that some value is invalid - we filter them out here and only return valid values. + // A common case is a missing/empty "Accept" header, in which case we just get 1 candidate of empty string (which is invalid). + if (MediaTypeWithQualityHeaderValue.TryParse(candidate, out var mediaType)) + yield return mediaType; } + } - public sealed class Settings + private ProtocolNegotiationResult NegotiateComminucationProtocol(HttpRequest request) + { + var acceptHeaderValues = request.Headers.Accept.ToString(); + + // We allow the "Accept" HTTP header to be overridden by the "accept" query string parameter. + // This is mainly for development purposes (to make it easier to request OpenMetrics format via browser URL bar). + if (request.Query.TryGetValue("accept", out var acceptValuesFromQuery)) + acceptHeaderValues = string.Join(",", acceptValuesFromQuery); + + foreach (var candidate in ExtractAcceptableMediaTypes(acceptHeaderValues) + .OrderByDescending(mt => mt.Quality.GetValueOrDefault(1))) { - public CollectorRegistry? Registry { get; set; } + if (candidate.MediaType == PrometheusConstants.TextContentType) + { + // The first preference is the text format. Fall throgh to the default case. + break; + } + else if (_enableOpenMetrics && candidate.MediaType == PrometheusConstants.OpenMetricsContentType) + { + return new ProtocolNegotiationResult(ExpositionFormat.OpenMetricsText, PrometheusConstants.OpenMetricsContentTypeWithVersionAndEncoding); + } } - private readonly CollectorRegistry _registry; + return new ProtocolNegotiationResult(ExpositionFormat.PrometheusText, PrometheusConstants.TextContentTypeWithVersionAndEncoding); + } + + public async Task Invoke(HttpContext context) + { + var response = context.Response; - public async Task Invoke(HttpContext context) + try { - var response = context.Response; + var negotiationResult = NegotiateComminucationProtocol(context.Request); - try - { - // We first touch the response.Body only in the callback because touching - // it means we can no longer send headers (the status code). - var serializer = new TextSerializer(delegate - { - response.ContentType = PrometheusConstants.ExporterContentType; - response.StatusCode = StatusCodes.Status200OK; - return response.Body; - }); - - await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted); - } - catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) + Stream GetResponseBodyStream() { - // The scrape was cancalled by the client. This is fine. Just swallow the exception to not generate pointless spam. + // We first touch the response.Body only in the callback here because touching it means we can no longer send headers (the status code). + // The collection logic will delay calling this method until it is reasonably confident that nothing will go wrong will the collection. + response.ContentType = negotiationResult.ContentType; + response.StatusCode = StatusCodes.Status200OK; + + return response.Body; } - catch (ScrapeFailedException ex) + + var serializer = new TextSerializer(GetResponseBodyStream, negotiationResult.ExpositionFormat); + + await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) + { + // The scrape was cancalled by the client. This is fine. Just swallow the exception to not generate pointless spam. + } + catch (ScrapeFailedException ex) + { + // This can only happen before any serialization occurs, in the pre-collect callbacks. + // So it should still be safe to update the status code and write an error message. + response.StatusCode = StatusCodes.Status503ServiceUnavailable; + + if (!string.IsNullOrWhiteSpace(ex.Message)) { - // This can only happen before any serialization occurs, in the pre-collect callbacks. - // So it should still be safe to update the status code and write an error message. - response.StatusCode = StatusCodes.Status503ServiceUnavailable; - - if (!string.IsNullOrWhiteSpace(ex.Message)) - { - using (var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, - bufferSize: -1, leaveOpen: true)) - await writer.WriteAsync(ex.Message); - } + using var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, bufferSize: -1, leaveOpen: true); + await writer.WriteAsync(ex.Message); } } } diff --git a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs index b1ab57a4..742befb7 100644 --- a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs @@ -1,83 +1,131 @@ -using System; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using System.ComponentModel; -namespace Prometheus +namespace Prometheus; + +public static class MetricServerMiddlewareExtensions { - public static class MetricServerMiddlewareExtensions + private const string DefaultDisplayName = "Prometheus metrics"; + + /// + /// Starts a Prometheus metrics exporter using endpoint routing. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + public static IEndpointConventionBuilder MapMetrics( + this IEndpointRouteBuilder endpoints, + Action configure, + string pattern = "/metrics" + ) { + var pipeline = endpoints + .CreateApplicationBuilder() + .InternalUseMiddleware(configure) + .Build(); + + return endpoints + .Map(pattern, pipeline) + .WithDisplayName(DefaultDisplayName); + } -#if NETCOREAPP3_1 + /// + /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + int port, + Action configure, + string? url = "/metrics") + { + // If no URL, use root URL. + url ??= "/"; - private const string DefaultDisplayName = "Prometheus metrics"; + return builder + .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(configure))); - /// - /// Starts a Prometheus metrics exporter using endpoint routing. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - public static IEndpointConventionBuilder MapMetrics( - this IEndpointRouteBuilder endpoints, - string pattern = "/metrics", - CollectorRegistry? registry = null - ) + Func PortMatches() { + return c => c.Connection.LocalPort == port; + } + } - var pipeline = endpoints - .CreateApplicationBuilder() - .UseMiddleware( - new MetricServerMiddleware.Settings - { - Registry = registry - } - ) - .Build(); + /// + /// Starts a Prometheus metrics exporter. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + Action configure, + string? url = "/metrics") + { + if (url != null) + return builder.Map(url, b => b.InternalUseMiddleware(configure)); + else + return builder.InternalUseMiddleware(configure); + } - return endpoints - .Map(pattern, pipeline) - .WithDisplayName(DefaultDisplayName); + #region Legacy methods without the configure action + /// + /// Starts a Prometheus metrics exporter using endpoint routing. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IEndpointConventionBuilder MapMetrics( + this IEndpointRouteBuilder endpoints, + string pattern = "/metrics", + CollectorRegistry? registry = null + ) + { + return MapMetrics(endpoints, LegacyConfigure(registry), pattern); + } - } + /// + /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + int port, + string? url = "/metrics", + CollectorRegistry? registry = null) + { + return UseMetricServer(builder, port, LegacyConfigure(registry), url); + } -#endif + /// + /// Starts a Prometheus metrics exporter. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + string? url = "/metrics", + CollectorRegistry? registry = null) + { + return UseMetricServer(builder, LegacyConfigure(registry), url); + } - /// - /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, int port, string? url = "/metrics", CollectorRegistry? registry = null) + private static Action LegacyConfigure(CollectorRegistry? registry) => + (MetricServerMiddleware.Settings settings) => { - return builder - .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(registry))); - - Func PortMatches() - { - return c => c.Connection.LocalPort == port; - } - } + settings.Registry = registry; + }; + #endregion - /// - /// Starts a Prometheus metrics exporter. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, string? url = "/metrics", CollectorRegistry? registry = null) - { - // If there is a URL to map, map it and re-enter without the URL. - if (url != null) - return builder.Map(url, b => b.InternalUseMiddleware(registry)); - else - return builder.InternalUseMiddleware(registry); - } + private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, Action configure) + { + var settings = new MetricServerMiddleware.Settings(); + configure(settings); - private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, CollectorRegistry? registry = null) - { - return builder.UseMiddleware(new MetricServerMiddleware.Settings - { - Registry = registry - }); - } + return builder.UseMiddleware(settings); } } diff --git a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj index aa85ffeb..8a84ff63 100644 --- a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj +++ b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj @@ -1,51 +1,75 @@  - - netstandard2.0;netcoreapp3.1 - Prometheus - - false - - true - - ..\Resources\prometheus-net.snk - - preview - enable - - true - - - - bin\Release\netstandard2.0\Prometheus.AspNetCore.xml - true - 1701;1702;1705;1591 - - - - bin\Debug\netstandard2.0\Prometheus.AspNetCore.xml - true - 1701;1702;1705;1591 - - - - - - - - - - - - - - - - - - - - - + + net6.0 + Prometheus + + false + + true + ..\Resources\prometheus-net.snk + + enable + enable + True + True + 1591 + + latest + 9999 + + True + + + true + true + + + prometheus-net.AspNetCore + andrasm,qed-,lakario,sandersaares + prometheus-net + prometheus-net + ASP.NET Core middleware and stand-alone Kestrel server for exporting metrics to Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + metrics prometheus aspnetcore + MIT + True + snupkg + + + + + true + + + + + True + \ + + + True + \ + + + + + + + + + + + + + + + + + + diff --git a/Prometheus.NetCore/DiagnosticSourceAdapter.cs b/Prometheus.NetCore/DiagnosticSourceAdapter.cs deleted file mode 100644 index 098744f5..00000000 --- a/Prometheus.NetCore/DiagnosticSourceAdapter.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Prometheus -{ - /// - /// Monitors all DiagnosticSource events and exposes them as Prometheus counters. - /// The event data is discarded, only the number of occurrences is measured. - /// - /// - /// This is a very coarse data set due to lacking any intelligence on the payload. - /// Users are recommended to make custom adapters with more detail for specific use cases. - /// - public sealed class DiagnosticSourceAdapter : IDisposable - { - /// - /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. - /// Dispose of the return value to stop listening. - /// - public static IDisposable StartListening() => StartListening(DiagnosticSourceAdapterOptions.Default); - - /// - /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. - /// Dispose of the return value to stop listening. - /// - public static IDisposable StartListening(DiagnosticSourceAdapterOptions options) => new DiagnosticSourceAdapter(options); - - private DiagnosticSourceAdapter(DiagnosticSourceAdapterOptions options) - { - _options = options; - _metric = Metrics.WithCustomRegistry(options.Registry) - .CreateCounter("diagnostic_events_total", "Total count of events received via the DiagnosticSource infrastructure.", new CounterConfiguration - { - LabelNames = new[] - { - "source", // Name of the DiagnosticSource - "event" // Name of the event - } - }); - - var newListenerObserver = new NewListenerObserver(OnNewListener); - _newListenerSubscription = DiagnosticListener.AllListeners.Subscribe(newListenerObserver); - } - - private readonly DiagnosticSourceAdapterOptions _options; - private readonly Counter _metric; - - private readonly IDisposable _newListenerSubscription; - - // listener name -> subscription - private readonly Dictionary _newEventSubscription = new Dictionary(); - private readonly object _newEventSubscriptionLock = new object(); - - private void OnNewListener(DiagnosticListener listener) - { - lock (_newEventSubscriptionLock) - { - if (_newEventSubscription.TryGetValue(listener.Name, out var oldSubscription)) - { - oldSubscription.Dispose(); - _newEventSubscription.Remove(listener.Name); - } - - if (!_options.ListenerFilterPredicate(listener)) - return; - - var listenerName = listener.Name; - var newEventObserver = new NewEventObserver(kvp => OnEvent(listenerName, kvp.Key, kvp.Value)); - _newEventSubscription[listenerName] = listener.Subscribe(newEventObserver); - } - } - - private void OnEvent(string listenerName, string eventName, object payload) - { - _metric.WithLabels(listenerName, eventName).Inc(); - } - - private sealed class NewListenerObserver : IObserver - { - private readonly Action _onNewListener; - - public NewListenerObserver(Action onNewListener) - { - _onNewListener = onNewListener; - } - - public void OnCompleted() - { - } - - public void OnError(Exception error) - { - } - - public void OnNext(DiagnosticListener listener) - { - _onNewListener(listener); - } - } - - private sealed class NewEventObserver : IObserver> - { - private readonly Action> _onEvent; - - public NewEventObserver(Action> onEvent) - { - _onEvent = onEvent; - } - - public void OnCompleted() - { - } - - public void OnError(Exception error) - { - } - - public void OnNext(KeyValuePair receivedEvent) - { - _onEvent(receivedEvent); - } - } - - public void Dispose() - { - _newListenerSubscription.Dispose(); - - lock (_newEventSubscriptionLock) - { - foreach (var subscription in _newEventSubscription.Values) - subscription.Dispose(); - } - } - } -} diff --git a/Prometheus.NetCore/DiagnosticSourceAdapterOptions.cs b/Prometheus.NetCore/DiagnosticSourceAdapterOptions.cs deleted file mode 100644 index 2b910720..00000000 --- a/Prometheus.NetCore/DiagnosticSourceAdapterOptions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Diagnostics; - -namespace Prometheus -{ - public sealed class DiagnosticSourceAdapterOptions - { - internal static readonly DiagnosticSourceAdapterOptions Default = new DiagnosticSourceAdapterOptions(); - - /// - /// By default we subscribe to all listeners but this allows you to filter by listener. - /// - public Func ListenerFilterPredicate = _ => true; - - public CollectorRegistry Registry = Metrics.DefaultRegistry; - } -} diff --git a/Prometheus.NetCore/Prometheus.NetCore.csproj b/Prometheus.NetCore/Prometheus.NetCore.csproj deleted file mode 100644 index 025735be..00000000 --- a/Prometheus.NetCore/Prometheus.NetCore.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - netcoreapp2.1 - - Prometheus - - false - - true - - ..\Resources\prometheus-net.snk - - preview - enable - - true - true - 1701;1702;1705;1591 - - - - bin\Release\netcoreapp2.1\Prometheus.NetCore.xml - - - - bin\Debug\netcoreapp2.1\Prometheus.NetCore.xml - - - - - - - - - - diff --git a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs index ff7a32df..a9d28c9d 100644 --- a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs +++ b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs @@ -1,67 +1,73 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.Net; using System.Web.Http; -namespace Prometheus +namespace Prometheus; + +public static class AspNetMetricServer { - public static class AspNetMetricServer + private const string RouteNamePrefix = "Prometheus_"; + + public sealed class Settings { - public sealed class Settings - { - public CollectorRegistry Registry { get; set; } - } + public CollectorRegistry? Registry { get; set; } + } - /// - /// Registers an anonymous instance of the controller to be published on the /metrics URL. - /// - public static void RegisterRoutes(HttpConfiguration configuration, Settings settings = null) - { - if (configuration == null) - throw new ArgumentNullException(nameof(configuration)); + /// + /// Registers an anonymous instance of the controller to be published on the /metrics URL. + /// + public static void RegisterRoutes(HttpConfiguration configuration, Settings? settings = null) => + MapRoute(configuration, "Default", "metrics", settings); - configuration.Routes.MapHttpRoute( - name: "Prometheus_Default", - routeTemplate: "metrics", - defaults: null, - constraints: null, - handler: new Handler(settings?.Registry ?? Metrics.DefaultRegistry)); - } + /// + /// Registers an anonymous instance of the controller to be published on a given URL path (e.g. "custom/metrics"). + /// + public static void RegisterRoutes(HttpConfiguration configuration, string path, Settings? settings = null) => + MapRoute(configuration, path, path, settings); + + private static void MapRoute(HttpConfiguration configuration, string routeName, string routeTemplate, Settings? settings) + { + if (configuration == null) + throw new ArgumentNullException(nameof(configuration)); - private sealed class Handler : HttpMessageHandler + configuration.Routes.MapHttpRoute( + name: $"{RouteNamePrefix}{routeName}", + routeTemplate: routeTemplate, + defaults: null, + constraints: null, + handler: new Handler(settings?.Registry ?? Metrics.DefaultRegistry)); + } + + private sealed class Handler : HttpMessageHandler + { + public Handler(CollectorRegistry registry) { - public Handler(CollectorRegistry registry) - { - _registry = registry; - } + _registry = registry; + } - private readonly CollectorRegistry _registry; + private readonly CollectorRegistry _registry; - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - // The ASP.NET PushStreamContent does not have a way to easily handle exceptions that - // occur before we write to the stream (when we can still return nice error headers). - // Maybe in a future version this could be improved, as right now exception == aborted connection. + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // The ASP.NET PushStreamContent does not have a way to easily handle exceptions that + // occur before we write to the stream (when we can still return nice error headers). + // Maybe in a future version this could be improved, as right now exception == aborted connection. - var response = new HttpResponseMessage(HttpStatusCode.OK) + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new PushStreamContent(async (stream, content, context) => { - Content = new PushStreamContent(async (stream, content, context) => + try + { + await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.PrometheusText, cancellationToken); + } + finally { - try - { - await _registry.CollectAndExportAsTextAsync(stream, cancellationToken); - } - finally - { - stream.Close(); - } - }, PrometheusConstants.ExporterContentTypeMinimal) - }; + stream.Close(); + } + }, PrometheusConstants.ExporterContentTypeValue) + }; - return Task.FromResult(response); - } + return Task.FromResult(response); } } } diff --git a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj index c4911aab..cdd99c2e 100644 --- a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj +++ b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj @@ -1,74 +1,73 @@ - - - - - Debug - AnyCPU - {25C05E15-A8F8-4F36-A35C-4004C0948305} - Library - Properties - Prometheus - Prometheus.NetFramework.AspNet - v4.6.1 - 512 - true - true - ..\Resources\prometheus-net.snk - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - true - true - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - true - true - - - - ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll - - - - - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll - - - ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll - - - - - - - - - - - Properties\SolutionAssemblyInfo.cs - - - - - - - - - {ba482927-f0bc-462c-8ba0-c476d93621bc} - Prometheus.NetStandard - - - - \ No newline at end of file + + + + netframework462 + Prometheus + + false + + true + ..\Resources\prometheus-net.snk + + enable + enable + True + True + 1591 + + latest + 9999 + + True + + + true + true + + + prometheus-net.NetFramework.AspNet + sandersaares + prometheus-net + prometheus-net + ASP.NET Web API exporter for Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + metrics prometheus aspnetcore + MIT + True + snupkg + + + + + true + + + + + True + \ + + + True + \ + + + + + + + + + + + + + + + + + + diff --git a/Prometheus.NetFramework.AspNet/Usings.cs b/Prometheus.NetFramework.AspNet/Usings.cs new file mode 100644 index 00000000..e6a9eb40 --- /dev/null +++ b/Prometheus.NetFramework.AspNet/Usings.cs @@ -0,0 +1 @@ +global using System.Net.Http; \ No newline at end of file diff --git a/Prometheus.NetFramework.AspNet/packages.config b/Prometheus.NetFramework.AspNet/packages.config deleted file mode 100644 index aeba5de5..00000000 --- a/Prometheus.NetFramework.AspNet/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Prometheus.NetStandard/ChildBase.cs b/Prometheus.NetStandard/ChildBase.cs deleted file mode 100644 index b7b762f2..00000000 --- a/Prometheus.NetStandard/ChildBase.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Base class for labeled instances of metrics (with all label names and label values defined). - /// - public abstract class ChildBase : ICollectorChild, IDisposable - { - internal ChildBase(Collector parent, Labels labels, Labels flattenedLabels, bool publish) - { - _parent = parent; - Labels = labels; - FlattenedLabels = flattenedLabels; - _publish = publish; - } - - /// - /// Marks the metric as one to be published, even if it might otherwise be suppressed. - /// - /// This is useful for publishing zero-valued metrics once you have loaded data on startup and determined - /// that there is no need to increment the value of the metric. - /// - /// - /// Subclasses must call this when their value is first set, to mark the metric as published. - /// - public void Publish() - { - Volatile.Write(ref _publish, true); - } - - /// - /// Marks the metric as one to not be published. - /// - /// The metric will be published when Publish() is called or the value is updated. - /// - public void Unpublish() - { - Volatile.Write(ref _publish, false); - } - - /// - /// Removes this labeled instance from metrics. - /// It will no longer be published and any existing measurements/buckets will be discarded. - /// - public void Remove() - { - _parent.RemoveLabelled(Labels); - } - - public void Dispose() => Remove(); - - /// - /// Labels specific to this metric instance, without any inherited static labels. - /// Internal for testing purposes only. - /// - internal Labels Labels { get; } - - /// - /// All labels that materialize on this metric instance, including inherited static labels. - /// Internal for testing purposes only. - /// - internal Labels FlattenedLabels { get; } - - private readonly Collector _parent; - - private bool _publish; - - /// - /// Collects all the metric data rows from this collector and serializes it using the given serializer. - /// - /// - /// Subclass must check _publish and suppress output if it is false. - /// - internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - if (!Volatile.Read(ref _publish)) - return Task.CompletedTask; - - return CollectAndSerializeImplAsync(serializer, cancel); - } - - // Same as above, just only called if we really need to serialize this metric (if publish is true). - private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); - - /// - /// Creates a metric identifier, with an optional name postfix and optional extra labels. - /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} - /// - protected byte[] CreateIdentifier(string? postfix = null, params (string, string)[] extraLabels) - { - var fullName = postfix != null ? $"{_parent.Name}_{postfix}" : _parent.Name; - - var labels = FlattenedLabels; - if (extraLabels?.Length > 0) - labels = FlattenedLabels.Concat(extraLabels); - - if (labels.Count != 0) - return PrometheusConstants.ExportEncoding.GetBytes($"{fullName}{{{labels.Serialize()}}}"); - else - return PrometheusConstants.ExportEncoding.GetBytes(fullName); - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/Collector.cs b/Prometheus.NetStandard/Collector.cs deleted file mode 100644 index 87b1eb7c..00000000 --- a/Prometheus.NetStandard/Collector.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Base class for metrics, defining the basic informative API and the internal API. - /// - public abstract class Collector - { - /// - /// The metric name, e.g. http_requests_total. - /// - public string Name { get; } - - /// - /// The help text describing the metric for a human audience. - /// - public string Help { get; } - - /// - /// Names of the instance-specific labels (name-value pairs) that apply to this metric. - /// When the values are added to the names, you get a instance. - /// - public string[] LabelNames { get; } - - internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel); - - // Used by ChildBase.Remove() - internal abstract void RemoveLabelled(Labels labels); - - private static readonly string[] EmptyLabelNames = new string[0]; - - private const string ValidMetricNameExpression = "^[a-zA-Z_:][a-zA-Z0-9_:]*$"; - private const string ValidLabelNameExpression = "^[a-zA-Z_:][a-zA-Z0-9_:]*$"; - private const string ReservedLabelNameExpression = "^__.*$"; - - private static readonly Regex MetricNameRegex = new Regex(ValidMetricNameExpression, RegexOptions.Compiled); - private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled); - private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled); - - protected Collector(string name, string help, string[]? labelNames) - { - labelNames ??= EmptyLabelNames; - - if (!MetricNameRegex.IsMatch(name)) - throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); - - foreach (var labelName in labelNames) - { - if (labelName == null) - throw new ArgumentNullException("Label name was null."); - - ValidateLabelName(labelName); - } - - Name = name; - Help = help; - LabelNames = labelNames; - } - - internal static void ValidateLabelName(string labelName) - { - if (!LabelNameRegex.IsMatch(labelName)) - throw new ArgumentException($"Label name '{labelName}' does not match regex '{ValidLabelNameExpression}'."); - - if (ReservedLabelRegex.IsMatch(labelName)) - throw new ArgumentException($"Label name '{labelName}' is not valid - labels starting with double underscore are reserved!"); - } - } - - /// - /// Base class for metrics collectors, providing common labeled child management functionality. - /// - public abstract class Collector : Collector, ICollector - where TChild : ChildBase - { - private readonly ConcurrentDictionary _labelledMetrics = new ConcurrentDictionary(); - - // Lazy-initialized since not every collector will use a child with no labels. - private readonly Lazy _unlabelledLazy; - - /// - /// Gets the child instance that has no labels. - /// - protected internal TChild Unlabelled => _unlabelledLazy.Value; - - // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured. - TChild ICollector.Unlabelled => Unlabelled; - - // This servers a slightly silly but useful purpose: by default if you start typing .La... and trigger Intellisense - // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration. - // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience. - public TChild WithLabels(params string[] labelValues) => Labels(labelValues); - - // Discourage it as it can create confusion. But it works fine, so no reason to mark it obsolete, really. - [EditorBrowsable(EditorBrowsableState.Never)] - public TChild Labels(params string[] labelValues) - { - var key = new Labels(LabelNames, labelValues); - return GetOrAddLabelled(key); - } - - public void RemoveLabelled(params string[] labelValues) - { - var key = new Labels(LabelNames, labelValues); - _labelledMetrics.TryRemove(key, out _); - } - - internal override void RemoveLabelled(Labels labels) - { - _labelledMetrics.TryRemove(labels, out _); - } - - /// - /// Gets the instance-specific label values of all labelled instances of the collector. - /// Values of any inherited static labels are not returned in the result. - /// - /// Note that during concurrent operation, the set of values returned here - /// may diverge from the latest set of values used by the collector. - /// - public IEnumerable GetAllLabelValues() - { - foreach (var labels in _labelledMetrics.Keys) - { - if (labels.Count == 0) - continue; // We do not return the "unlabelled" label set. - - // Defensive copy. - yield return labels.Values.ToArray(); - } - } - - /// - /// Set of static labels obtained from any hierarchy level (either defined in metric configuration or in registry). - /// - private readonly Labels _staticLabels; - - private TChild GetOrAddLabelled(Labels key) - { - // Don't allocate lambda for GetOrAdd in the common case that the labeled metrics exist. - if (_labelledMetrics.TryGetValue(key, out var metric)) - return metric; - - return _labelledMetrics.GetOrAdd(key, k => NewChild(k, k.Concat(_staticLabels), publish: !_suppressInitialValue)); - } - - /// - /// For tests that want to see what label values were used when metrics were created. - /// - internal Labels[] GetAllLabels() => _labelledMetrics.Select(p => p.Key).ToArray(); - - internal Collector(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue) - : base(name, help, labelNames) - { - _staticLabels = staticLabels; - _suppressInitialValue = suppressInitialValue; - _unlabelledLazy = new Lazy(() => GetOrAddLabelled(Prometheus.Labels.Empty)); - - // Check for label name collisions. - var allLabelNames = (labelNames ?? new string[0]).Concat(staticLabels.Names).ToList(); - if (allLabelNames.Count() != allLabelNames.Distinct(StringComparer.Ordinal).Count()) - throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", allLabelNames)); - - _familyHeaderLines = new byte[][] - { - PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"), - PrometheusConstants.ExportEncoding.GetBytes($"# TYPE {name} {Type.ToString().ToLowerInvariant()}") - }; - } - - /// - /// Creates a new instance of the child collector type. - /// - private protected abstract TChild NewChild(Labels labels, Labels flattenedLabels, bool publish); - - private protected abstract MetricType Type { get; } - - private readonly byte[][] _familyHeaderLines; - - internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - EnsureUnlabelledMetricCreatedIfNoLabels(); - - await serializer.WriteFamilyDeclarationAsync(_familyHeaderLines, cancel); - - foreach (var child in _labelledMetrics.Values) - await child.CollectAndSerializeAsync(serializer, cancel); - } - - private readonly bool _suppressInitialValue; - - private void EnsureUnlabelledMetricCreatedIfNoLabels() - { - // We want metrics to exist even with 0 values if they are supposed to be used without labels. - // Labelled metrics are created when label values are assigned. However, as unlabelled metrics are lazy-created - // (they might are optional if labels are used) we might lose them for cases where they really are desired. - - // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it. - // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default. - if (!_unlabelledLazy.IsValueCreated && !LabelNames.Any()) - GetOrAddLabelled(Prometheus.Labels.Empty); - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/CollectorRegistry.cs b/Prometheus.NetStandard/CollectorRegistry.cs deleted file mode 100644 index f9aa04a4..00000000 --- a/Prometheus.NetStandard/CollectorRegistry.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Maintains references to a set of collectors, from which data for metrics is collected at data export time. - /// - /// Use methods on the class to add metrics to a collector registry. - /// - /// - /// To encourage good concurrency practices, registries are append-only. You can add things to them but not remove. - /// If you wish to remove things from the registry, create a new registry with only the things you wish to keep. - /// - public sealed class CollectorRegistry : ICollectorRegistry - { - #region "Before collect" callbacks - /// - /// Registers an action to be called before metrics are collected. - /// This enables you to do last-minute updates to metric values very near the time of collection. - /// Callbacks will delay the metric collection, so do not make them too long or it may time out. - /// - /// The callback will be executed synchronously and should not take more than a few milliseconds. - /// To execute longer-duration callbacks, register an asynchronous callback (Func<Task>). - /// - /// If the callback throws then the entire metric collection will fail. - /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. - /// - /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. - /// - public void AddBeforeCollectCallback(Action callback) - { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); - - _beforeCollectCallbacks.Add(callback); - } - - /// - /// Registers an action to be called before metrics are collected. - /// This enables you to do last-minute updates to metric values very near the time of collection. - /// Callbacks will delay the metric collection, so do not make them too long or it may time out. - /// - /// Asynchronous callbacks will be executed concurrently and may last longer than a few milliseconds. - /// - /// If the callback throws then the entire metric collection will fail. - /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. - /// - /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. - /// - public void AddBeforeCollectCallback(Func callback) - { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); - - _beforeCollectAsyncCallbacks.Add(callback); - } - - private readonly ConcurrentBag _beforeCollectCallbacks = new ConcurrentBag(); - private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = new ConcurrentBag>(); - #endregion - - #region Static labels - /// - /// The set of static labels that are applied to all metrics in this registry. - /// Enumeration of the returned collection is thread-safe. - /// - public IEnumerable> StaticLabels => _staticLabels; - - /// - /// Defines the set of static labels to apply to all metrics in this registry. - /// The static labels can only be set once on startup, before adding or publishing any metrics. - /// - public void SetStaticLabels(Dictionary labels) - { - if (labels == null) - throw new ArgumentNullException(nameof(labels)); - - // Read lock is taken when creating metrics, so we know that no metrics can be created while we hold this lock. - _staticLabelsLock.EnterWriteLock(); - - try - { - if (_staticLabels.Length != 0) - throw new InvalidOperationException("Static labels have already been defined - you can only do it once per registry."); - - if (_collectors.Count != 0) - throw new InvalidOperationException("Metrics have already been added to the registry - cannot define static labels anymore."); - - // Keep the lock for the duration of this method to make sure no publishing happens while we are setting labels. - lock (_firstCollectLock) - { - if (_hasPerformedFirstCollect) - throw new InvalidOperationException("The metrics registry has already been published - cannot define static labels anymore."); - - foreach (var pair in labels) - { - if (pair.Key == null) - throw new ArgumentException("The name of a label cannot be null."); - - if (pair.Value == null) - throw new ArgumentException("The value of a label cannot be null."); - - Collector.ValidateLabelName(pair.Key); - } - - _staticLabels = labels.ToArray(); - } - } - finally - { - _staticLabelsLock.ExitWriteLock(); - } - } - - private KeyValuePair[] _staticLabels = new KeyValuePair[0]; - private readonly ReaderWriterLockSlim _staticLabelsLock = new ReaderWriterLockSlim(); - - /// - /// Executes an action while holding a read lock on the set of static labels (provided as parameter). - /// - internal TReturn WhileReadingStaticLabels(Func action) - { - _staticLabelsLock.EnterReadLock(); - - try - { - var labels = new Labels(_staticLabels.Select(item => item.Key).ToArray(), _staticLabels.Select(item => item.Value).ToArray()); - - return action(labels); - } - finally - { - _staticLabelsLock.ExitReadLock(); - } - } - #endregion - - /// - /// Collects all metrics and exports them in text document format to the provided stream. - /// - /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. - /// - public Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default) - { - if (to == null) - throw new ArgumentNullException(nameof(to)); - - return CollectAndSerializeAsync(new TextSerializer(to), cancel); - } - - - // We pass this thing to GetOrAdd to avoid allocating a collector or a closure. - // This reduces memory usage in situations where the collector is already registered. - internal readonly struct CollectorInitializer - where TCollector : Collector - where TConfiguration : MetricConfiguration - { - private readonly Func _createInstance; - private readonly string _name; - private readonly string _help; - private readonly TConfiguration _configuration; - - public string Name => _name; - public TConfiguration Configuration => _configuration; - - public CollectorInitializer(Func createInstance, - string name, string help, TConfiguration configuration) - { - _createInstance = createInstance; - _name = name; - _help = help; - _configuration = configuration; - } - - public TCollector CreateInstance(string _) => _createInstance(_name, _help, _configuration); - } - - /// - /// Adds a collector to the registry, returning an existing instance if one with a matching name was already registered. - /// - internal TCollector GetOrAdd(in CollectorInitializer initializer) - where TCollector : Collector - where TConfiguration : MetricConfiguration - { - var collectorToUse = _collectors.GetOrAdd(initializer.Name, initializer.CreateInstance); - - if (!(collectorToUse is TCollector)) - throw new InvalidOperationException("Collector of a different type with the same name is already registered."); - - if ((initializer.Configuration.LabelNames?.Length ?? 0) != collectorToUse.LabelNames.Length - || (!initializer.Configuration.LabelNames?.SequenceEqual(collectorToUse.LabelNames) ?? false)) - throw new InvalidOperationException("Collector matches a previous registration but has a different set of label names."); - - return (TCollector)collectorToUse; - } - - private readonly ConcurrentDictionary _collectors = new ConcurrentDictionary(); - - internal void SetBeforeFirstCollectCallback(Action a) - { - lock (_firstCollectLock) - { - if (_hasPerformedFirstCollect) - return; // Avoid keeping a reference to a callback we won't ever use. - - _beforeFirstCollectCallback = a; - } - } - - /// - /// Allows us to initialize (or not) the registry with the default metrics before the first collection. - /// - private Action? _beforeFirstCollectCallback; - private bool _hasPerformedFirstCollect; - private readonly object _firstCollectLock = new object(); - - /// - /// Collects metrics from all the registered collectors and sends them to the specified serializer. - /// - internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - lock (_firstCollectLock) - { - if (!_hasPerformedFirstCollect) - { - _hasPerformedFirstCollect = true; - _beforeFirstCollectCallback?.Invoke(); - _beforeFirstCollectCallback = null; - } - } - - foreach (var callback in _beforeCollectCallbacks) - callback(); - - await Task.WhenAll(_beforeCollectAsyncCallbacks.Select(callback => callback(cancel))); - - foreach (var collector in _collectors.Values) - await collector.CollectAndSerializeAsync(serializer, cancel); - - await serializer.FlushAsync(cancel); - } - } -} diff --git a/Prometheus.NetStandard/Counter.cs b/Prometheus.NetStandard/Counter.cs deleted file mode 100644 index ed84eef3..00000000 --- a/Prometheus.NetStandard/Counter.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - public sealed class Counter : Collector, ICounter - { - public sealed class Child : ChildBase, ICounter - { - internal Child(Collector parent, Labels labels, Labels flattenedLabels, bool publish) - : base(parent, labels, flattenedLabels, publish) - { - _identifier = CreateIdentifier(); - } - - private readonly byte[] _identifier; - - private ThreadSafeDouble _value; - - private protected override Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - return serializer.WriteMetricAsync(_identifier, Value, cancel); - } - - public void Inc(double increment = 1.0) - { - if (increment < 0.0) - throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); - - _value.Add(increment); - Publish(); - } - - public void IncTo(double targetValue) - { - _value.IncrementTo(targetValue); - Publish(); - } - - public double Value => _value.Value; - } - - private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish) - { - return new Child(this, labels, flattenedLabels, publish); - } - - internal Counter(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue) - : base(name, help, labelNames, staticLabels, suppressInitialValue) - { - } - - public void Inc(double increment = 1) => Unlabelled.Inc(increment); - public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); - public double Value => Unlabelled.Value; - - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); - - private protected override MetricType Type => MetricType.Counter; - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/CounterConfiguration.cs b/Prometheus.NetStandard/CounterConfiguration.cs deleted file mode 100644 index 2cbc42b7..00000000 --- a/Prometheus.NetStandard/CounterConfiguration.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Prometheus -{ - public sealed class CounterConfiguration : MetricConfiguration - { - internal static readonly CounterConfiguration Default = new CounterConfiguration(); - } -} diff --git a/Prometheus.NetStandard/CounterExtensions.cs b/Prometheus.NetStandard/CounterExtensions.cs deleted file mode 100644 index 0d410fe5..00000000 --- a/Prometheus.NetStandard/CounterExtensions.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Prometheus -{ - public static class CounterExtensions - { - /// - /// Increments the value of the counter to the current UTC time as a Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToCurrentTimeUtc(this ICounter counter) - { - counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); - } - - /// - /// Increments the value of the counter to a specific moment as the UTC Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToTimeUtc(this ICounter counter, DateTimeOffset timestamp) - { - counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); - } - - /// - /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static void CountExceptions(this ICounter counter, Action wrapped, Func? exceptionFilter = null) - { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); - - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); - - try - { - wrapped(); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } - } - - /// - /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static TResult CountExceptions(this ICounter counter, Func wrapped, Func? exceptionFilter = null) - { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); - - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); - - try - { - return wrapped(); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } - } - - /// - /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static async Task CountExceptionsAsync(this ICounter counter, Func wrapped, Func? exceptionFilter = null) - { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); - - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); - - try - { - await wrapped().ConfigureAwait(false); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } - } - - /// - /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static async Task CountExceptionsAsync(this ICounter counter, Func> wrapped, Func? exceptionFilter = null) - { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); - - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); - - try - { - return await wrapped().ConfigureAwait(false); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } - } - } -} diff --git a/Prometheus.NetStandard/DelegatingStreamInternal.cs b/Prometheus.NetStandard/DelegatingStreamInternal.cs deleted file mode 100644 index 24406006..00000000 --- a/Prometheus.NetStandard/DelegatingStreamInternal.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// Modified for prometheus-net to reduce dependencies. - -#nullable enable -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net.Http -{ - // Forwards all calls to an inner stream except where overridden in a derived class. - internal abstract class DelegatingStreamInternal : Stream - { - private readonly Stream _innerStream; - - #region Properties - - public override bool CanRead - { - get { return _innerStream.CanRead; } - } - - public override bool CanSeek - { - get { return _innerStream.CanSeek; } - } - - public override bool CanWrite - { - get { return _innerStream.CanWrite; } - } - - public override long Length - { - get { return _innerStream.Length; } - } - - public override long Position - { - get { return _innerStream.Position; } - set { _innerStream.Position = value; } - } - - public override int ReadTimeout - { - get { return _innerStream.ReadTimeout; } - set { _innerStream.ReadTimeout = value; } - } - - public override bool CanTimeout - { - get { return _innerStream.CanTimeout; } - } - - public override int WriteTimeout - { - get { return _innerStream.WriteTimeout; } - set { _innerStream.WriteTimeout = value; } - } - - #endregion Properties - - protected DelegatingStreamInternal(Stream innerStream) - { - _innerStream = innerStream; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _innerStream.Dispose(); - } - base.Dispose(disposing); - } - - - public override long Seek(long offset, SeekOrigin origin) - { - return _innerStream.Seek(offset, origin); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _innerStream.Read(buffer, offset, count); - } - - public override int ReadByte() - { - return _innerStream.ReadByte(); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); - } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - return _innerStream.BeginRead(buffer, offset, count, callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return _innerStream.EndRead(asyncResult); - } - - public override void Flush() - { - _innerStream.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _innerStream.FlushAsync(cancellationToken); - } - - public override void SetLength(long value) - { - _innerStream.SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _innerStream.Write(buffer, offset, count); - } - - public override void WriteByte(byte value) - { - _innerStream.WriteByte(value); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); - } - - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - return _innerStream.BeginWrite(buffer, offset, count, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - _innerStream.EndWrite(asyncResult); - } - - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); - } - } -} diff --git a/Prometheus.NetStandard/DotNetStats.cs b/Prometheus.NetStandard/DotNetStats.cs deleted file mode 100644 index c2d3b761..00000000 --- a/Prometheus.NetStandard/DotNetStats.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Prometheus -{ - /// - /// Collects basic .NET metrics about the current process. This is not meant to be an especially serious collector, - /// more of a producer of sample data so users of the library see something when they install it. - /// - public sealed class DotNetStats - { - /// - /// Registers the .NET metrics in the specified registry. - /// - public static void Register(CollectorRegistry registry) - { - var instance = new DotNetStats(registry); - registry.AddBeforeCollectCallback(instance.UpdateMetrics); - } - - private readonly Process _process; - private readonly List _collectionCounts = new List(); - private Gauge _totalMemory; - private Gauge _virtualMemorySize; - private Gauge _workingSet; - private Gauge _privateMemorySize; - private Counter _cpuTotal; - private Gauge _openHandles; - private Gauge _startTime; - private Gauge _numThreads; - - private DotNetStats(CollectorRegistry registry) - { - _process = Process.GetCurrentProcess(); - var metrics = Metrics.WithCustomRegistry(registry); - - var collectionCountsParent = metrics.CreateCounter("dotnet_collection_count_total", "GC collection count", new[] { "generation" }); - - for (var gen = 0; gen <= GC.MaxGeneration; gen++) - { - _collectionCounts.Add(collectionCountsParent.Labels(gen.ToString())); - } - - // Metrics that make sense to compare between all operating systems - // Note that old versions of pushgateway errored out if different metrics had same name but different help string. - // This is fixed in newer versions but keep the help text synchronized with the Go implementation just in case. - // See https://github.com/prometheus/pushgateway/issues/194 - // and https://github.com/prometheus-net/prometheus-net/issues/89 - _startTime = metrics.CreateGauge("process_start_time_seconds", "Start time of the process since unix epoch in seconds."); - _cpuTotal = metrics.CreateCounter("process_cpu_seconds_total", "Total user and system CPU time spent in seconds."); - - _virtualMemorySize = metrics.CreateGauge("process_virtual_memory_bytes", "Virtual memory size in bytes."); - _workingSet = metrics.CreateGauge("process_working_set_bytes", "Process working set"); - _privateMemorySize = metrics.CreateGauge("process_private_memory_bytes", "Process private memory size"); - _openHandles = metrics.CreateGauge("process_open_handles", "Number of open handles"); - _numThreads = metrics.CreateGauge("process_num_threads", "Total number of threads"); - - // .net specific metrics - _totalMemory = metrics.CreateGauge("dotnet_total_memory_bytes", "Total known allocated memory"); - - var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - _startTime.Set((_process.StartTime.ToUniversalTime() - epoch).TotalSeconds); - } - - // The Process class is not thread-safe so let's synchronize the updates to avoid data tearing. - private readonly object _updateLock = new object(); - - private void UpdateMetrics() - { - try - { - lock (_updateLock) - { - _process.Refresh(); - - for (var gen = 0; gen <= GC.MaxGeneration; gen++) - { - var collectionCount = _collectionCounts[gen]; - collectionCount.Inc(GC.CollectionCount(gen) - collectionCount.Value); - } - - _totalMemory.Set(GC.GetTotalMemory(false)); - _virtualMemorySize.Set(_process.VirtualMemorySize64); - _workingSet.Set(_process.WorkingSet64); - _privateMemorySize.Set(_process.PrivateMemorySize64); - _cpuTotal.Inc(Math.Max(0, _process.TotalProcessorTime.TotalSeconds - _cpuTotal.Value)); - _openHandles.Set(_process.HandleCount); - _numThreads.Set(_process.Threads.Count); - } - } - catch (Exception) - { - } - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/Gauge.cs b/Prometheus.NetStandard/Gauge.cs deleted file mode 100644 index 7b852cfa..00000000 --- a/Prometheus.NetStandard/Gauge.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - public sealed class Gauge : Collector, IGauge - { - public sealed class Child : ChildBase, IGauge - { - internal Child(Collector parent, Labels labels, Labels flattenedLabels, bool publish) - : base(parent, labels, flattenedLabels, publish) - { - _identifier = CreateIdentifier(); - } - - private readonly byte[] _identifier; - - private ThreadSafeDouble _value; - - private protected override Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - return serializer.WriteMetricAsync(_identifier, Value, cancel); - } - - public void Inc(double increment = 1) - { - _value.Add(increment); - Publish(); - } - - public void Set(double val) - { - _value.Value = val; - Publish(); - } - - public void Dec(double decrement = 1) - { - Inc(-decrement); - } - - public void IncTo(double targetValue) - { - _value.IncrementTo(targetValue); - Publish(); - } - - public void DecTo(double targetValue) - { - _value.DecrementTo(targetValue); - Publish(); - } - - public double Value => _value.Value; - } - - private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish) - { - return new Child(this, labels, flattenedLabels, publish); - } - - internal Gauge(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue) - : base(name, help, labelNames, staticLabels, suppressInitialValue) - { - } - - public void Inc(double increment = 1) => Unlabelled.Inc(increment); - public void Set(double val) => Unlabelled.Set(val); - public void Dec(double decrement = 1) => Unlabelled.Dec(decrement); - public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); - public void DecTo(double targetValue) => Unlabelled.DecTo(targetValue); - public double Value => Unlabelled.Value; - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); - - private protected override MetricType Type => MetricType.Gauge; - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/GaugeConfiguration.cs b/Prometheus.NetStandard/GaugeConfiguration.cs deleted file mode 100644 index ca732340..00000000 --- a/Prometheus.NetStandard/GaugeConfiguration.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Prometheus -{ - public sealed class GaugeConfiguration : MetricConfiguration - { - internal static readonly GaugeConfiguration Default = new GaugeConfiguration(); - } -} diff --git a/Prometheus.NetStandard/GaugeExtensions.cs b/Prometheus.NetStandard/GaugeExtensions.cs deleted file mode 100644 index 146794bf..00000000 --- a/Prometheus.NetStandard/GaugeExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; - -namespace Prometheus -{ - public static class GaugeExtensions - { - /// - /// Sets the value of the gauge to the current UTC time as a Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// - public static void SetToCurrentTimeUtc(this IGauge gauge) - { - gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); - } - - /// - /// Sets the value of the gauge to a specific moment as the UTC timezone Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// - public static void SetToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) - { - gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); - } - - /// - /// Increments the value of the gauge to the current UTC time as a Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToCurrentTimeUtc(this IGauge gauge) - { - gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); - } - - /// - /// Increments the value of the gauge to a specific moment as the UTC Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) - { - gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); - } - - private sealed class InProgressTracker : IDisposable - { - public InProgressTracker(IGauge gauge) - { - _gauge = gauge; - } - - public void Dispose() - { - _gauge.Dec(); - } - - private readonly IGauge _gauge; - } - - /// - /// Tracks the number of in-progress operations taking place. - /// - /// Calling this increments the gauge. Disposing of the returned instance decrements it again. - /// - /// - /// It is safe to track the sum of multiple concurrent in-progress operations with the same gauge. - /// - public static IDisposable TrackInProgress(this IGauge gauge) - { - if (gauge == null) - throw new ArgumentNullException(nameof(gauge)); - - gauge.Inc(); - - return new InProgressTracker(gauge); - } - } -} diff --git a/Prometheus.NetStandard/Histogram.cs b/Prometheus.NetStandard/Histogram.cs deleted file mode 100644 index 1f8eae2b..00000000 --- a/Prometheus.NetStandard/Histogram.cs +++ /dev/null @@ -1,256 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// The histogram is thread-safe but not atomic - the sum of values and total count of events - /// may not add up perfectly with bucket contents if new observations are made during a collection. - /// - public sealed class Histogram : Collector, IHistogram - { - private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 }; - private readonly double[] _buckets; - - internal Histogram(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue, double[]? buckets) - : base(name, help, labelNames, staticLabels, suppressInitialValue) - { - if (labelNames?.Any(l => l == "le") == true) - { - throw new ArgumentException("'le' is a reserved label name"); - } - _buckets = buckets ?? DefaultBuckets; - - if (_buckets.Length == 0) - { - throw new ArgumentException("Histogram must have at least one bucket"); - } - - if (!double.IsPositiveInfinity(_buckets[_buckets.Length - 1])) - { - _buckets = _buckets.Concat(new[] { double.PositiveInfinity }).ToArray(); - } - - for (int i = 1; i < _buckets.Length; i++) - { - if (_buckets[i] <= _buckets[i - 1]) - { - throw new ArgumentException("Bucket values must be increasing"); - } - } - } - - private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish) - { - return new Child(this, labels, flattenedLabels, publish); - } - - public sealed class Child : ChildBase, IHistogram - { - internal Child(Histogram parent, Labels labels, Labels flattenedLabels, bool publish) - : base(parent, labels, flattenedLabels, publish) - { - _parent = parent; - - _upperBounds = _parent._buckets; - _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; - - _sumIdentifier = CreateIdentifier("sum"); - _countIdentifier = CreateIdentifier("count"); - - _bucketIdentifiers = new byte[_upperBounds.Length][]; - for (var i = 0; i < _upperBounds.Length; i++) - { - var value = double.IsPositiveInfinity(_upperBounds[i]) ? "+Inf" : _upperBounds[i].ToString(CultureInfo.InvariantCulture); - - _bucketIdentifiers[i] = CreateIdentifier("bucket", ("le", value)); - } - } - - private readonly Histogram _parent; - - private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); - private readonly ThreadSafeLong[] _bucketCounts; - private readonly double[] _upperBounds; - - internal readonly byte[] _sumIdentifier; - internal readonly byte[] _countIdentifier; - internal readonly byte[][] _bucketIdentifiers; - - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - // We output sum. - // We output count. - // We output each bucket in order of increasing upper bound. - - await serializer.WriteMetricAsync(_sumIdentifier, _sum.Value, cancel); - await serializer.WriteMetricAsync(_countIdentifier, _bucketCounts.Sum(b => b.Value), cancel); - - var cumulativeCount = 0L; - - for (var i = 0; i < _bucketCounts.Length; i++) - { - cumulativeCount += _bucketCounts[i].Value; - - await serializer.WriteMetricAsync(_bucketIdentifiers[i], cumulativeCount, cancel); - } - } - - public double Sum => _sum.Value; - public long Count => _bucketCounts.Sum(b => b.Value); - - public void Observe(double val) => Observe(val, 1); - - public void Observe(double val, long count) - { - if (double.IsNaN(val)) - { - return; - } - - for (int i = 0; i < _upperBounds.Length; i++) - { - if (val <= _upperBounds[i]) - { - _bucketCounts[i].Add(count); - break; - } - } - _sum.Add(val * count); - Publish(); - } - } - - private protected override MetricType Type => MetricType.Histogram; - - public double Sum => Unlabelled.Sum; - public long Count => Unlabelled.Count; - public void Observe(double val) => Unlabelled.Observe(val, 1); - public void Observe(double val, long count) => Unlabelled.Observe(val, count); - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); - - // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go - /// - /// Creates '' buckets, where the lowest bucket has an - /// upper bound of '' and each following bucket's upper bound is '' - /// times the previous bucket's upper bound. - /// - /// The function throws if '' is 0 or negative, if '' is 0 or negative, - /// or if '' is less than or equal 1. - /// - /// The upper bound of the lowest bucket. Must be positive. - /// The factor to increase the upper bound of subsequent buckets. Must be greater than 1. - /// The number of buckets to create. Must be positive. - public static double[] ExponentialBuckets(double start, double factor, int count) - { - if (count <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(count)}"); - if (start <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(start)}"); - if (factor <= 1) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a {nameof(factor)} greater than 1"); - - // The math we do can make it incur some tiny avoidable error due to floating point gremlins. - // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. - // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. - - var next = (decimal)start; - var buckets = new double[count]; - - for (var i = 0; i < buckets.Length; i++) - { - buckets[i] = (double)next; - next *= (decimal)factor; - } - - return buckets; - } - - // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go - /// - /// Creates '' buckets, where the lowest bucket has an - /// upper bound of '' and each following bucket's upper bound is the upper bound of the - /// previous bucket, incremented by '' - /// - /// The function throws if '' is 0 or negative. - /// - /// The upper bound of the lowest bucket. - /// The width of each bucket (distance between lower and upper bound). - /// The number of buckets to create. Must be positive. - public static double[] LinearBuckets(double start, double width, int count) - { - if (count <= 0) throw new ArgumentException($"{nameof(LinearBuckets)} needs a positive {nameof(count)}"); - - // The math we do can make it incur some tiny avoidable error due to floating point gremlins. - // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. - // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. - - var next = (decimal)start; - var buckets = new double[count]; - - for (var i = 0; i < buckets.Length; i++) - { - buckets[i] = (double)next; - next += (decimal)width; - } - - return buckets; - } - - /// - /// Divides each power of 10 into N divisions. - /// - /// The starting range includes 10 raised to this power. - /// The ranges end with 10 raised to this power (this no longer starts a new range). - /// How many divisions to divide each range into. - /// - /// For example, with startPower=-1, endPower=2, divisions=4 we would get: - /// 10^-1 == 0.1 which defines our starting range, giving buckets: 0.25, 0.5, 0.75, 1.0 - /// 10^0 == 1 which is the next range, giving buckets: 2.5, 5, 7.5, 10 - /// 10^1 == 10 which is the next range, giving buckets: 25, 50, 75, 100 - /// 10^2 == 100 which is the end and the top level of the preceding range. - /// Giving total buckets: 0.25, 0.5, 0.75, 1.0, 2.5, 5, 7.5, 10, 25, 50, 75, 100 - /// - public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, int divisions) - { - if (startPower >= endPower) - throw new ArgumentException($"{nameof(startPower)} must be less than {nameof(endPower)}.", nameof(startPower)); - - if (divisions <= 0) - throw new ArgumentOutOfRangeException($"{nameof(divisions)} must be a positive integer.", nameof(divisions)); - - var buckets = new List(); - - for (var powerOfTen = startPower; powerOfTen < endPower; powerOfTen++) - { - // This gives us the upper bound (the start of the next range). - var max = (decimal)Math.Pow(10, powerOfTen + 1); - - // Then we just divide it into N divisions and we are done! - for (var division = 0; division < divisions; division++) - { - var bucket = max / divisions * (division + 1); - - // The math we do can make it incur some tiny avoidable error due to floating point gremlins. - // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. - // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. - var candidate = (double)bucket; - - // Depending on the number of divisions, it may be that divisions from different powers overlap. - // For example, a division into 20 would include: - // 19th value in the 0th power: 9.5 (10/20*19=9.5) - // 1st value in the 1st power: 5 (100/20*1 = 5) - // To avoid this being a problem, we simply constrain all values to be increasing. - if (buckets.Any() && buckets.Last() >= candidate) - continue; // Skip this one, it is not greater. - - buckets.Add(candidate); - } - } - - return buckets.ToArray(); - } - } -} diff --git a/Prometheus.NetStandard/HistogramConfiguration.cs b/Prometheus.NetStandard/HistogramConfiguration.cs deleted file mode 100644 index 893c499d..00000000 --- a/Prometheus.NetStandard/HistogramConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Prometheus -{ - public sealed class HistogramConfiguration : MetricConfiguration - { - internal static readonly HistogramConfiguration Default = new HistogramConfiguration(); - - /// - /// Custom histogram buckets to use. If null, will use Histogram.DefaultBuckets. - /// - public double[]? Buckets { get; set; } - } -} diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs deleted file mode 100644 index 587980c6..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http; - -namespace Prometheus.HttpClientMetrics -{ - /// - /// This base class performs the data management necessary to associate the correct labels and values - /// with HttpClient metrics, depending on the options the user has provided for the HttpClient metric handler. - /// - /// The following labels are supported: - /// 'method' (HTTP request method) - /// 'host' (The host name of HTTP request) - /// 'client' (The name of the HttpClient) - /// 'code' (HTTP response status code) - /// - internal abstract class HttpClientDelegatingHandlerBase : DelegatingHandler - where TCollector : class, ICollector - where TChild : class, ICollectorChild - { - /// - /// The set of labels from among the defaults that this metric supports. - /// - protected abstract string[] DefaultLabels { get; } - - /// - /// The factory to use for creating the default metric for this middleware. - /// Not used if a custom metric is already provided in options. - /// - protected MetricFactory MetricFactory { get; } - - /// - /// Creates the default metric instance with the specified set of labels. - /// Only used if the caller does not provide a custom metric instance in the options. - /// - protected abstract TCollector CreateMetricInstance(string[] labelNames); - - // Internal only for tests. - internal readonly TCollector _metric; - - protected HttpClientDelegatingHandlerBase(HttpClientMetricsOptionsBase? options, TCollector? customMetric, HttpClientIdentity identity) - { - _identity = identity; - - MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); - - if (customMetric != null) - { - _metric = customMetric; - - ValidateNoUnexpectedLabelNames(); - } - else - { - _metric = CreateMetricInstance(HttpClientRequestLabelNames.All); - } - } - - private readonly HttpClientIdentity _identity; - - /// - /// Creates the metric child instance to use for measurements. - /// - /// - /// Internal for testing purposes. - /// - protected internal TChild CreateChild(HttpRequestMessage request, HttpResponseMessage? response) - { - if (!_metric.LabelNames.Any()) - return _metric.Unlabelled; - - var labelValues = new string[_metric.LabelNames.Length]; - - for (var i = 0; i < labelValues.Length; i++) - { - switch (_metric.LabelNames[i]) - { - case HttpClientRequestLabelNames.Method: - labelValues[i] = request.Method.Method; - break; - case HttpClientRequestLabelNames.Host: - labelValues[i] = request.RequestUri.Host; - break; - case HttpClientRequestLabelNames.Client: - labelValues[i] = _identity.Name; - break; - case HttpClientRequestLabelNames.Code: - labelValues[i] = response != null ? ((int)response.StatusCode).ToString() : ""; - break; - default: - // We validate the label set on initialization, so this is impossible. - throw new NotSupportedException($"Found unsupported label on metric: {_metric.LabelNames[i]}"); - } - } - - return _metric.WithLabels(labelValues); - } - - /// - /// If we use a custom metric, it should not have labels that are not among the defaults. - /// - private void ValidateNoUnexpectedLabelNames() - { - var allowedLabels = HttpClientRequestLabelNames.All; - var unexpected = _metric.LabelNames.Except(allowedLabels); - - if (unexpected.Any()) - throw new ArgumentException($"Provided custom HttpClient metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientExporterOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientExporterOptions.cs deleted file mode 100644 index 6f58df20..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientExporterOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public sealed class HttpClientExporterOptions - { - public HttpClientInProgressOptions InProgress { get; set; } = new HttpClientInProgressOptions(); - public HttpClientRequestCountOptions RequestCount { get; set; } = new HttpClientRequestCountOptions(); - public HttpClientRequestDurationOptions RequestDuration { get; set; } = new HttpClientRequestDurationOptions(); - public HttpClientResponseDurationOptions ResponseDuration { get; set; } = new HttpClientResponseDurationOptions(); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientIdentity.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientIdentity.cs deleted file mode 100644 index 595e1ebe..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientIdentity.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public sealed class HttpClientIdentity - { - public static readonly HttpClientIdentity Default = new HttpClientIdentity("default"); - - public string Name { get; } - - public HttpClientIdentity(string name) - { - Name = name; - } - } -} diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressHandler.cs deleted file mode 100644 index 292b62c5..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus.HttpClientMetrics -{ - internal sealed class HttpClientInProgressHandler : HttpClientDelegatingHandlerBase, IGauge> - { - public HttpClientInProgressHandler(HttpClientInProgressOptions? options, HttpClientIdentity identity) - : base(options, options?.Gauge, identity) - { - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - using (CreateChild(request, null).TrackInProgress()) - { - return await base.SendAsync(request, cancellationToken); - } - } - - protected override string[] DefaultLabels => HttpClientRequestLabelNames.KnownInAdvance; - - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( - "httpclient_requests_in_progress", - "Number of requests currently being executed by an HttpClient.", - new GaugeConfiguration - { - LabelNames = labelNames - }); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressOptions.cs deleted file mode 100644 index 68853edb..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public sealed class HttpClientInProgressOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Gauge { get; set; } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientMetricsOptionsBase.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientMetricsOptionsBase.cs deleted file mode 100644 index 95192635..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientMetricsOptionsBase.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public abstract class HttpClientMetricsOptionsBase - { - public bool Enabled { get; set; } = true; - - /// - /// Allows you to override the registry used to create the default metric instance. - /// Value is ignored if you specify a custom metric instance in the options. - /// - public CollectorRegistry? Registry { get; set; } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountHandler.cs deleted file mode 100644 index 409f0299..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus.HttpClientMetrics -{ - internal sealed class HttpClientRequestCountHandler : HttpClientDelegatingHandlerBase, ICounter> - { - public HttpClientRequestCountHandler(HttpClientRequestCountOptions? options, HttpClientIdentity identity) - : base(options, options?.Counter, identity) - { - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - HttpResponseMessage? response = null; - - try - { - response = await base.SendAsync(request, cancellationToken); - return response; - } - finally - { - CreateChild(request, response).Inc(); - } - } - - protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; - - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( - "httpclient_requests_sent_total", - "Count of HTTP requests that have been completed by an HttpClient.", - new CounterConfiguration - { - LabelNames = labelNames - }); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountOptions.cs deleted file mode 100644 index 5985c2fb..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public sealed class HttpClientRequestCountOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Counter { get; set; } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationHandler.cs deleted file mode 100644 index ddec4e7f..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus.HttpClientMetrics -{ - internal sealed class HttpClientRequestDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> - { - public HttpClientRequestDurationHandler(HttpClientRequestDurationOptions? options, HttpClientIdentity identity) - : base(options, options?.Histogram, identity) - { - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var stopWatch = ValueStopwatch.StartNew(); - - HttpResponseMessage? response = null; - - try - { - // We measure until SendAsync returns - which is when the response HEADERS are seen. - // The response body may continue streaming for a long time afterwards, which this does not measure. - response = await base.SendAsync(request, cancellationToken); - return response; - } - finally - { - CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); - } - } - - protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; - - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( - "httpclient_request_duration_seconds", - "Duration histogram of HTTP requests performed by an HttpClient.", - new HistogramConfiguration - { - // 1 ms to 32K ms buckets - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), - LabelNames = labelNames - }); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationOptions.cs deleted file mode 100644 index 8177e968..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public sealed class HttpClientRequestDurationOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Histogram { get; set; } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestLabelNames.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestLabelNames.cs deleted file mode 100644 index a06a119d..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestLabelNames.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - /// - /// Label names reserved for the use by the HttpClient metrics. - /// - public static class HttpClientRequestLabelNames - { - public const string Method = "method"; - public const string Host = "host"; - public const string Client = "client"; - public const string Code = "code"; - - public static readonly string[] All = - { - Method, - Host, - Client, - Code - }; - - // The labels known before receiving the response. - // Everything except the response status code, basically. - public static readonly string[] KnownInAdvance = - { - Method, - Host, - Client - }; - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationHandler.cs deleted file mode 100644 index ff235638..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationHandler.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus.HttpClientMetrics -{ - internal sealed class HttpClientResponseDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> - { - public HttpClientResponseDurationHandler(HttpClientResponseDurationOptions? options, HttpClientIdentity identity) - : base(options, options?.Histogram, identity) - { - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var stopWatch = ValueStopwatch.StartNew(); - - var response = await base.SendAsync(request, cancellationToken); - - Stream oldStream = await response.Content.ReadAsStreamAsync(); - - Wrap(response, oldStream, delegate - { - CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); - }); - - return response; - } - - protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; - - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( - "httpclient_response_duration_seconds", - "Duration histogram of HTTP requests performed by an HttpClient, measuring the duration until the HTTP response finished being processed.", - new HistogramConfiguration - { - // 1 ms to 32K ms buckets - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), - LabelNames = labelNames - }); - - private void Wrap(HttpResponseMessage response, Stream oldStream, Action onEndOfStream) - { - var newContent = new StreamContent(new EndOfStreamDetectingStream(oldStream, onEndOfStream)); - - var oldHeaders = response.Content.Headers; - var newHeaders = newContent.Headers; - -#if NET6_0_OR_GREATER - foreach (KeyValuePair header in oldHeaders.NonValidated) - { - if (header.Value.Count > 1) - { - newHeaders.TryAddWithoutValidation(header.Key, header.Value); - } - else - { - newHeaders.TryAddWithoutValidation(header.Key, header.Value.ToString()); - } - } -#else - foreach (var header in oldHeaders) - { - newHeaders.TryAddWithoutValidation(header.Key, header.Value); - } -#endif - - response.Content = newContent; - } - - private sealed class EndOfStreamDetectingStream : Stream - { - public EndOfStreamDetectingStream(Stream inner, Action onEndOfStream) - { - _inner = inner; - _onEndOfStream = onEndOfStream; - } - - private readonly Stream _inner; - private readonly Action _onEndOfStream; - private int _sawEndOfStream = 0; - - public override void Flush() => _inner.Flush(); - - public override int Read(byte[] buffer, int offset, int count) - { - var read = _inner.Read(buffer, offset, count); - - if (read == 0 && buffer.Length != 0) - { - SignalCompletion(); - } - - return read; - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return buffer.Length == 0 - ? _inner.ReadAsync(buffer, offset, count, cancellationToken) - : ReadAsyncCore(this, _inner.ReadAsync(buffer, offset, count, cancellationToken)); - - static async Task ReadAsyncCore(EndOfStreamDetectingStream stream, Task readTask) - { - int read = await readTask; - - if (read == 0) - { - stream.SignalCompletion(); - } - - return read; - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - SignalCompletion(); - - _inner.Dispose(); - } - } - - private void SignalCompletion() - { - if (Interlocked.Exchange(ref _sawEndOfStream, 1) == 0) - { - _onEndOfStream(); - } - } - - public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin); - public override void SetLength(long value) => _inner.SetLength(value); - public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count); - public override bool CanRead => _inner.CanRead; - public override bool CanSeek => _inner.CanSeek; - public override bool CanWrite => _inner.CanWrite; - public override long Length => _inner.Length; - public override long Position { get => _inner.Position; set => _inner.Position = value; } - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationOptions.cs deleted file mode 100644 index 0bd2c68f..00000000 --- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Prometheus.HttpClientMetrics -{ - public sealed class HttpClientResponseDurationOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Histogram { get; set; } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/HttpClientMetricsExtensions.cs b/Prometheus.NetStandard/HttpClientMetricsExtensions.cs deleted file mode 100644 index 2abcbfd4..00000000 --- a/Prometheus.NetStandard/HttpClientMetricsExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Prometheus.HttpClientMetrics; -using System; - -namespace Prometheus -{ - public static class HttpClientMetricsExtensions - { - /// - /// Configures the HttpClient pipeline to collect Prometheus metrics. - /// - public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, Action configure) - { - var options = new HttpClientExporterOptions(); - - configure?.Invoke(options); - - builder.UseHttpClientMetrics(options); - - return builder; - } - - /// - /// Configures the HttpClient pipeline to collect Prometheus metrics. - /// - public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, HttpClientExporterOptions? options = null) - { - options ??= new HttpClientExporterOptions(); - - var identity = new HttpClientIdentity(builder.Name); - - if (options.InProgress.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientInProgressHandler(options.InProgress, identity)); - } - - if (options.RequestCount.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientRequestCountHandler(options.RequestCount, identity)); - } - - if (options.RequestDuration.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientRequestDurationHandler(options.RequestDuration, identity)); - } - - if (options.ResponseDuration.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); - } - - return builder; - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/ICollector.cs b/Prometheus.NetStandard/ICollector.cs deleted file mode 100644 index 011f2c49..00000000 --- a/Prometheus.NetStandard/ICollector.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Child-type-specific interface implemented by all collectors, used to enable substitution in test code. - /// - public interface ICollector : ICollector - where TChild : ICollectorChild - { - TChild Unlabelled { get; } - TChild WithLabels(params string[] labelValues); - } - - /// - /// Interface implemented by all collectors, used to enable substitution in test code. - /// - public interface ICollector - { - string Name { get; } - string Help { get; } - string[] LabelNames { get; } - } -} diff --git a/Prometheus.NetStandard/ICollectorChild.cs b/Prometheus.NetStandard/ICollectorChild.cs deleted file mode 100644 index 422ea653..00000000 --- a/Prometheus.NetStandard/ICollectorChild.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Prometheus -{ - /// - /// Interface shared by all labelled collector children. - /// - public interface ICollectorChild - { - } -} diff --git a/Prometheus.NetStandard/ICollectorRegistry.cs b/Prometheus.NetStandard/ICollectorRegistry.cs deleted file mode 100644 index a9a5f176..00000000 --- a/Prometheus.NetStandard/ICollectorRegistry.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Allows for substitution of CollectorRegistry in tests. - /// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code. - /// - public interface ICollectorRegistry - { - void AddBeforeCollectCallback(Action callback); - void AddBeforeCollectCallback(Func callback); - - IEnumerable> StaticLabels { get; } - void SetStaticLabels(Dictionary labels); - - Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default); - } -} diff --git a/Prometheus.NetStandard/ICounter.cs b/Prometheus.NetStandard/ICounter.cs deleted file mode 100644 index 8803d4be..00000000 --- a/Prometheus.NetStandard/ICounter.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Prometheus -{ - public interface ICounter : ICollectorChild - { - void Inc(double increment = 1); - void IncTo(double targetValue); - double Value { get; } - } -} diff --git a/Prometheus.NetStandard/IGauge.cs b/Prometheus.NetStandard/IGauge.cs deleted file mode 100644 index ac5c50fd..00000000 --- a/Prometheus.NetStandard/IGauge.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Prometheus -{ - public interface IGauge : ICollectorChild - { - void Inc(double increment = 1); - void Set(double val); - void Dec(double decrement = 1); - void IncTo(double targetValue); - void DecTo(double targetValue); - double Value { get; } - } -} diff --git a/Prometheus.NetStandard/IHistogram.cs b/Prometheus.NetStandard/IHistogram.cs deleted file mode 100644 index 83c88dc9..00000000 --- a/Prometheus.NetStandard/IHistogram.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Prometheus -{ - public interface IHistogram : IObserver - { - /// - /// Observe multiple events with a given value. - /// - /// Intended to support high frequency or batch processing use cases utilizing pre-aggregation. - /// - /// Measured value. - /// Number of observations with this value. - void Observe(double val, long count); - - /// - /// Gets the sum of all observed events. - /// - double Sum { get; } - - /// - /// Gets the count of all observed events. - /// - long Count { get; } - } -} diff --git a/Prometheus.NetStandard/IMetricFactory.cs b/Prometheus.NetStandard/IMetricFactory.cs deleted file mode 100644 index e4522638..00000000 --- a/Prometheus.NetStandard/IMetricFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Prometheus -{ - /// - /// Allows for substitution of MetricFactory in tests. - /// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code. - /// - public interface IMetricFactory - { - Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null); - Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null); - Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null); - Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null); - } -} diff --git a/Prometheus.NetStandard/IMetricServer.cs b/Prometheus.NetStandard/IMetricServer.cs deleted file mode 100644 index 40c85e67..00000000 --- a/Prometheus.NetStandard/IMetricServer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// A metric server exposes a Prometheus metric exporter endpoint in the background, - /// operating independently and serving metrics until it is instructed to stop. - /// - public interface IMetricServer : IDisposable - { - /// - /// Starts serving metrics. - /// - /// Returns the same instance that was called (for fluent-API-style chaining). - /// - IMetricServer Start(); - - /// - /// Instructs the metric server to stop and returns a task you can await for it to stop. - /// - Task StopAsync(); - - /// - /// Instructs the metric server to stop and waits for it to stop. - /// - void Stop(); - } -} diff --git a/Prometheus.NetStandard/IMetricsSerializer.cs b/Prometheus.NetStandard/IMetricsSerializer.cs deleted file mode 100644 index 469f3a6e..00000000 --- a/Prometheus.NetStandard/IMetricsSerializer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// The only purpose this serves is to warn the developer when he might be accidentally introducing - /// new serialization-time relationships. The serialization code is very tied to the text format and - /// not intended to be a generic serialization mechanism. - /// - internal interface IMetricsSerializer - { - /// - /// Writes the lines that declare the metric family. - /// - Task WriteFamilyDeclarationAsync(byte[][] headerLines, CancellationToken cancel); - - /// - /// Writes a single metric in a metric family. - /// - Task WriteMetricAsync(byte[] identifier, double value, CancellationToken cancel); - - /// - /// Flushes any pending buffers. Always call this after all your write calls. - /// - Task FlushAsync(CancellationToken cancel); - } -} diff --git a/Prometheus.NetStandard/IObserver.cs b/Prometheus.NetStandard/IObserver.cs deleted file mode 100644 index 70b70393..00000000 --- a/Prometheus.NetStandard/IObserver.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Prometheus -{ - /// - /// Implemented by metric types that observe individual events with specific values. - /// - public interface IObserver : ICollectorChild - { - /// - /// Observes a single event with the given value. - /// - void Observe(double val); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/ISummary.cs b/Prometheus.NetStandard/ISummary.cs deleted file mode 100644 index aeed5840..00000000 --- a/Prometheus.NetStandard/ISummary.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Prometheus -{ - public interface ISummary : IObserver - { - } -} diff --git a/Prometheus.NetStandard/ITimer.cs b/Prometheus.NetStandard/ITimer.cs deleted file mode 100644 index b87894f1..00000000 --- a/Prometheus.NetStandard/ITimer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Prometheus -{ - /// - /// A timer that can be used to observe a duration of elapsed time. - /// - /// The observation is made either when ObserveDuration is called or when the instance is disposed of. - /// - public interface ITimer : IDisposable - { - /// - /// Observes the duration (in seconds) and returns the observed value. - /// - TimeSpan ObserveDuration(); - } -} diff --git a/Prometheus.NetStandard/Labels.cs b/Prometheus.NetStandard/Labels.cs deleted file mode 100644 index 31846bc1..00000000 --- a/Prometheus.NetStandard/Labels.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Linq; - -namespace Prometheus -{ - /// - /// The set of labels and label values associated with a metric. Used both for export and as keys. - /// - /// - /// Only the values are considered for equality purposes - the caller must ensure that - /// LabelValues objects with different sets of names are never compared to each other. - /// - /// Always use the explicit constructor when creating an instance. This is a struct in order - /// to reduce heap allocations when dealing with labelled metrics, which has the consequence of - /// adding a default parameterless constructor. It should not be used. - /// - internal struct Labels : IEquatable - { - public static readonly Labels Empty = new Labels(new string[0], new string[0]); - - public int Count => Names.Length; - - public string[] Values { get; } - public string[] Names { get; } - - private readonly int _hashCode; - - public Labels(string[] names, string[] values) - { - if (names == null) - throw new ArgumentNullException(nameof(names)); - - if (values == null) - throw new ArgumentNullException(nameof(values)); - - if (names.Length != values.Length) - throw new ArgumentException("The list of label values must have the same number of elements as the list of label names."); - - foreach (var lv in values) - { - if (lv == null) - throw new ArgumentNullException("A label value cannot be null."); - } - - Values = values; - Names = names; - - // Calculating the hash code is fast but we don't need to re-calculate it for each comparison. - // Labels are fixed - calculate it once up-front and remember the value. - _hashCode = CalculateHashCode(Values); - } - - public Labels Concat(params (string, string)[] more) - { - var moreLabels = new Labels(more.Select(pair => pair.Item1).ToArray(), more.Select(pair => pair.Item2).ToArray()); - - return Concat(moreLabels); - } - - public Labels Concat(Labels more) - { - var allNames = Names.Concat(more.Names).ToArray(); - var allValues = Values.Concat(more.Values).ToArray(); - - if (allNames.Length != allNames.Distinct(StringComparer.Ordinal).Count()) - throw new InvalidOperationException("The metric instance received multiple copies of the same label."); - - return new Labels(allNames, allValues); - } - - private static string EscapeLabelValue(string value) - { - return value - .Replace("\\", @"\\") - .Replace("\n", @"\n") - .Replace("\"", @"\"""); - } - - /// - /// Serializes to the labelkey1="labelvalue1",labelkey2="labelvalue2" label string. - /// - public string Serialize() - { - // Result is cached in child collector - no need to worry about efficiency here. - - var labels = Names - .Zip(Values, (name, value) => $"{name}=\"{EscapeLabelValue(value)}\""); - - return string.Join(",", labels); - } - - public bool Equals(Labels other) - { - if (_hashCode != other._hashCode) return false; - if (other.Values.Length != Values.Length) return false; - - for (int i = 0; i < Values.Length; i++) - { - if (!string.Equals(Values[i], other.Values[i], StringComparison.Ordinal)) - return false; - } - - return true; - } - - public override bool Equals(object obj) - { - if (!(obj is Labels)) - { - return false; - } - - var other = (Labels)obj; - return Equals(other); - } - - public override int GetHashCode() - { - return _hashCode; - } - - private static int CalculateHashCode(string[] values) - { - unchecked - { - int hashCode = 0; - for (int i = 0; i < values.Length; i++) - { - hashCode ^= (values[i].GetHashCode() * 397); - } - - return hashCode; - } - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/MetricConfiguration.cs b/Prometheus.NetStandard/MetricConfiguration.cs deleted file mode 100644 index a60d681b..00000000 --- a/Prometheus.NetStandard/MetricConfiguration.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; - -namespace Prometheus -{ - /// - /// This class packages the options for creating metrics into a single class (with subclasses per metric type) - /// for easy extensibility of the API without adding numerous method overloads whenever new options are added. - /// - public abstract class MetricConfiguration - { - /// - /// Names of all the label fields that are defined for each instance of the metric. - /// If null, the metric will be created without any instance-specific labels. - /// - /// Before using a metric that uses instance-specific labels, .WithLabels() must be called to provide values for the labels. - /// - public string[]? LabelNames { get; set; } - - /// - /// The static labels to apply to all instances of this metric. These labels cannot be later overwritten. - /// - public Dictionary? StaticLabels { get; set; } - - /// - /// If true, the metric will not be published until its value is first modified (regardless of the specific value). - /// This is useful to delay publishing gauges that get their initial values delay-loaded. - /// - /// By default, metrics are published as soon as possible - if they do not use labels then they are published on - /// creation and if they use labels then as soon as the label values are assigned. - /// - public bool SuppressInitialValue { get; set; } - } -} diff --git a/Prometheus.NetStandard/MetricFactory.cs b/Prometheus.NetStandard/MetricFactory.cs deleted file mode 100644 index 0a3b2358..00000000 --- a/Prometheus.NetStandard/MetricFactory.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Linq; - -namespace Prometheus -{ - /// - /// Adds metrics to a registry. - /// - public sealed class MetricFactory : IMetricFactory - { - private readonly CollectorRegistry _registry; - - internal MetricFactory(CollectorRegistry registry) - { - _registry = registry ?? throw new ArgumentNullException(nameof(registry)); - } - - private Labels CreateStaticLabels(MetricConfiguration metricConfiguration) - { - return _registry.WhileReadingStaticLabels(registryLabels => - { - if (metricConfiguration.StaticLabels == null) - return registryLabels; - - var metricLabels = new Labels(metricConfiguration.StaticLabels.Keys.ToArray(), metricConfiguration.StaticLabels.Values.ToArray()); - - return metricLabels.Concat(registryLabels); - }); - } - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) - { - return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer( - (n, h, config) => new Counter(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue), - name, help, configuration ?? CounterConfiguration.Default)); - } - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) - { - return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer( - (n, h, config) => new Gauge(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue), - name, help, configuration ?? GaugeConfiguration.Default)); - } - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) - { - return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer( - (n, h, config) => new Summary(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue, config.Objectives, config.MaxAge, config.AgeBuckets, config.BufferSize), - name, help, configuration ?? SummaryConfiguration.Default)); - } - - /// - /// Histograms track the size and number of events in buckets. - /// - public Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) - { - return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer( - (n, h, config) => new Histogram(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue, config.Buckets), - name, help, configuration ?? HistogramConfiguration.Default)); - } - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public Counter CreateCounter(string name, string help, params string[] labelNames) => - CreateCounter(name, help, new CounterConfiguration - { - LabelNames = labelNames - }); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public Gauge CreateGauge(string name, string help, params string[] labelNames) => - CreateGauge(name, help, new GaugeConfiguration - { - LabelNames = labelNames - }); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public Summary CreateSummary(string name, string help, params string[] labelNames) => - CreateSummary(name, help, new SummaryConfiguration - { - LabelNames = labelNames - }); - - /// - /// Histograms track the size and number of events in buckets. - /// - public Histogram CreateHistogram(string name, string help, params string[] labelNames) => - CreateHistogram(name, help, new HistogramConfiguration - { - LabelNames = labelNames - }); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/MetricHandler.cs b/Prometheus.NetStandard/MetricHandler.cs deleted file mode 100644 index 249233d7..00000000 --- a/Prometheus.NetStandard/MetricHandler.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Base class for various metric server implementations that start an independent exporter in the background. - /// The expoters may either be pull-based (exposing the Prometheus API) or push-based (actively pushing to PushGateway). - /// - public abstract class MetricHandler : IMetricServer, IDisposable - { - // The registry that contains the collectors to export metrics from. - // Subclasses are expected to use this variable to obtain the correct registry. - protected readonly CollectorRegistry _registry; - - // The token is cancelled when the handler is instructed to stop. - private CancellationTokenSource? _cts = new CancellationTokenSource(); - - // This is the task started for the purpose of exporting metrics. - private Task? _task; - - protected MetricHandler(CollectorRegistry? registry = null) - { - _registry = registry ?? Metrics.DefaultRegistry; - } - - public IMetricServer Start() - { - if (_task != null) - throw new InvalidOperationException("The metric server has already been started."); - - if (_cts == null) - throw new InvalidOperationException("The metric server has already been started and stopped. Create a new server if you want to start it again."); - - _task = StartServer(_cts.Token); - return this; - } - - public async Task StopAsync() - { - // Signal the CTS to give a hint to the server thread that it is time to close up shop. - _cts?.Cancel(); - - try - { - if (_task == null) - return; // Never started. - - // This will re-throw any exception that was caught on the StartServerAsync thread. - // Perhaps not ideal behavior but hey, if the implementation does not want this to happen - // it should have caught it itself in the background processing thread. - await _task.ConfigureAwait(false); // Issue #308 - } - catch (OperationCanceledException) - { - // We'll eat this one, though, since it can easily get thrown by whatever checks the CancellationToken. - } - finally - { - _cts?.Dispose(); - _cts = null; - } - } - - public void Stop() - { - // This method mainly exists for API compatiblity with prometheus-net v1. But it works, so that's fine. - StopAsync().GetAwaiter().GetResult(); - } - - void IDisposable.Dispose() - { - Stop(); - } - - protected abstract Task StartServer(CancellationToken cancel); - } -} diff --git a/Prometheus.NetStandard/MetricPusher.cs b/Prometheus.NetStandard/MetricPusher.cs deleted file mode 100644 index 5da436c0..00000000 --- a/Prometheus.NetStandard/MetricPusher.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// A metric server that regularly pushes metrics to a Prometheus PushGateway. - /// - public class MetricPusher : MetricHandler - { - private readonly TimeSpan _pushInterval; - private readonly Uri _targetUrl; - private readonly Func _httpClientProvider; - - public MetricPusher(string endpoint, string job, string? instance = null, long intervalMilliseconds = 1000, IEnumerable>? additionalLabels = null, CollectorRegistry? registry = null) : this(new MetricPusherOptions - { - Endpoint = endpoint, - Job = job, - Instance = instance, - IntervalMilliseconds = intervalMilliseconds, - AdditionalLabels = additionalLabels, - Registry = registry - }) - { - } - - public MetricPusher(MetricPusherOptions options) : base(options.Registry) - { - if (string.IsNullOrEmpty(options.Endpoint)) - throw new ArgumentNullException(nameof(options.Endpoint)); - - if (string.IsNullOrEmpty(options.Job)) - throw new ArgumentNullException(nameof(options.Job)); - - if (options.IntervalMilliseconds <= 0) - throw new ArgumentException("Interval must be greater than zero", nameof(options.IntervalMilliseconds)); - - _httpClientProvider = options.HttpClientProvider ?? (() => _singletonHttpClient); - - StringBuilder sb = new StringBuilder(string.Format("{0}/job/{1}", options.Endpoint!.TrimEnd('/'), options.Job)); - if (!string.IsNullOrEmpty(options.Instance)) - sb.AppendFormat("/instance/{0}", options.Instance); - - if (options.AdditionalLabels != null) - { - foreach (var pair in options.AdditionalLabels) - { - if (pair == null || string.IsNullOrEmpty(pair.Item1) || string.IsNullOrEmpty(pair.Item2)) - throw new NotSupportedException($"Invalid {nameof(MetricPusher)} additional label: ({pair?.Item1}):({pair?.Item2})"); - - sb.AppendFormat("/{0}/{1}", pair.Item1, pair.Item2); - } - } - - if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out _targetUrl)) - { - throw new ArgumentException("Endpoint must be a valid url", "endpoint"); - } - - _pushInterval = TimeSpan.FromMilliseconds(options.IntervalMilliseconds); - _onError = options.OnError; - } - - private static readonly MediaTypeHeaderValue ContentTypeHeaderValue = new MediaTypeHeaderValue(PrometheusConstants.ExporterContentTypeMinimal); - - private static readonly HttpClient _singletonHttpClient = new HttpClient(); - - private readonly Action? _onError; - - protected override Task StartServer(CancellationToken cancel) - { - // Start the server processing loop asynchronously in the background. - return Task.Run(async delegate - { - while (true) - { - // We schedule approximately at the configured interval. There may be some small accumulation for the - // part of the loop we do not measure but it is close enough to be acceptable for all practical scenarios. - var duration = ValueStopwatch.StartNew(); - - try - { - var httpClient = _httpClientProvider(); - - // We use a copy-pasted implementation of PushStreamContent here to avoid taking a dependency on the old ASP.NET Web API where it lives. - var response = await httpClient.PostAsync(_targetUrl, new PushStreamContentInternal(async (stream, content, context) => - { - try - { - // Do not pass CT because we only want to cancel after pushing, so a flush is always performed. - await _registry.CollectAndExportAsTextAsync(stream, default); - } - finally - { - stream.Close(); - } - }, ContentTypeHeaderValue)); - - // If anything goes wrong, we want to get at least an entry in the trace log. - response.EnsureSuccessStatusCode(); - } - catch (ScrapeFailedException ex) - { - // We do not consider failed scrapes a reportable error since the user code that raises the failure should be the one logging it. - Trace.WriteLine($"Skipping metrics push due to failed scrape: {ex.Message}"); - } - catch (Exception ex) - { - HandleFailedPush(ex); - } - - // We stop only after pushing metrics, to ensure that the latest state is flushed when told to stop. - if (cancel.IsCancellationRequested) - break; - - var sleepTime = _pushInterval - duration.GetElapsedTime(); - - // Sleep until the interval elapses or the pusher is asked to shut down. - if (sleepTime > TimeSpan.Zero) - { - try - { - await Task.Delay(sleepTime, cancel); - } - catch (OperationCanceledException) - { - // The task was cancelled. - // We continue the loop here to ensure final state gets pushed. - continue; - } - } - } - }); - } - - private void HandleFailedPush(Exception ex) - { - if (_onError != null) - { - // Asynchronous because we don't trust the callee to be fast. - Task.Run(() => _onError(ex)); - } - else - { - // If there is no error handler registered, we write to trace to at least hopefully get some attention to the problem. - Trace.WriteLine(string.Format("Error in MetricPusher: {0}", ex)); - } - } - } -} diff --git a/Prometheus.NetStandard/MetricPusherOptions.cs b/Prometheus.NetStandard/MetricPusherOptions.cs deleted file mode 100644 index 676447cb..00000000 --- a/Prometheus.NetStandard/MetricPusherOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; - -namespace Prometheus -{ - public sealed class MetricPusherOptions - { - internal static readonly MetricPusherOptions Default = new MetricPusherOptions(); - - public string? Endpoint { get; set; } - public string? Job { get; set; } - public string? Instance { get; set; } - public long IntervalMilliseconds { get; set; } = 1000; - public IEnumerable>? AdditionalLabels { get; set; } - public CollectorRegistry? Registry { get; set; } - - /// - /// Callback for when a metric push fails. - /// - public Action? OnError { get; set; } - - /// - /// If null, a singleton HttpClient will be used. - /// - public Func? HttpClientProvider { get; set; } - } -} diff --git a/Prometheus.NetStandard/MetricServer.cs b/Prometheus.NetStandard/MetricServer.cs deleted file mode 100644 index ab2197f0..00000000 --- a/Prometheus.NetStandard/MetricServer.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Implementation of a Prometheus exporter that serves metrics using HttpListener. - /// This is a stand-alone exporter for apps that do not already have an HTTP server included. - /// - public class MetricServer : MetricHandler - { - private readonly HttpListener _httpListener = new HttpListener(); - - /// - /// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks. - /// By default (if null), all requests are served. - /// - public Func? RequestPredicate { get; set; } - - public MetricServer(int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : this("+", port, url, registry, useHttps) - { - } - - public MetricServer(string hostname, int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : base(registry) - { - var s = useHttps ? "s" : ""; - _httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}"); - } - - protected override Task StartServer(CancellationToken cancel) - { - // This will ensure that any failures to start are nicely thrown from StartServerAsync. - _httpListener.Start(); - - // Kick off the actual processing to a new thread and return a Task for the processing thread. - return Task.Factory.StartNew(delegate - { - try - { - Thread.CurrentThread.Name = "Metric Server"; //Max length 16 chars (Linux limitation) - - while (!cancel.IsCancellationRequested) - { - // There is no way to give a CancellationToken to GCA() so, we need to hack around it a bit. - var getContext = _httpListener.GetContextAsync(); - getContext.Wait(cancel); - var context = getContext.Result; - - // Asynchronously process the request. - _ = Task.Factory.StartNew(async delegate - { - var request = context.Request; - var response = context.Response; - - try - { - var predicate = RequestPredicate; - - if (predicate != null && !predicate(request)) - { - // Request rejected by predicate. - response.StatusCode = (int)HttpStatusCode.Forbidden; - return; - } - - try - { - // We first touch the response.OutputStream only in the callback because touching - // it means we can no longer send headers (the status code). - var serializer = new TextSerializer(delegate - { - response.ContentType = PrometheusConstants.ExporterContentType; - response.StatusCode = 200; - return response.OutputStream; - }); - - await _registry.CollectAndSerializeAsync(serializer, cancel); - response.OutputStream.Dispose(); - } - catch (ScrapeFailedException ex) - { - // This can only happen before anything is written to the stream, so it - // should still be safe to update the status code and report an error. - response.StatusCode = 503; - - if (!string.IsNullOrWhiteSpace(ex.Message)) - { - using (var writer = new StreamWriter(response.OutputStream)) - writer.Write(ex.Message); - } - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - if (!_httpListener.IsListening) - return; // We were shut down. - - Trace.WriteLine(string.Format("Error in {0}: {1}", nameof(MetricServer), ex)); - - try - { - response.StatusCode = 500; - } - catch - { - // Might be too late in request processing to set response code, so just ignore. - } - } - finally - { - response.Close(); - } - }); - } - } - finally - { - _httpListener.Stop(); - // This should prevent any currently processed requests from finishing. - _httpListener.Close(); - } - }, TaskCreationOptions.LongRunning); - } - } -} diff --git a/Prometheus.NetStandard/MetricType.cs b/Prometheus.NetStandard/MetricType.cs deleted file mode 100644 index 75ca46a9..00000000 --- a/Prometheus.NetStandard/MetricType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Prometheus -{ - internal enum MetricType - { - Counter, - Gauge, - Summary, - Histogram - } -} diff --git a/Prometheus.NetStandard/Metrics.cs b/Prometheus.NetStandard/Metrics.cs deleted file mode 100644 index 0f8665b1..00000000 --- a/Prometheus.NetStandard/Metrics.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Prometheus -{ - /// - /// Static class for easy creation of metrics. Acts as the entry point to the prometheus-net metrics recording API. - /// - /// Some built-in metrics are registered by default in the default collector registry. This is mainly to ensure that - /// the library exports some metrics when installed. If these default metrics are not desired, call - /// to remove them before registering your own. - /// - public static class Metrics - { - /// - /// The default registry where all metrics are registered by default. - /// - public static CollectorRegistry DefaultRegistry { get; private set; } - - private static MetricFactory _defaultFactory; - - /// - /// Creates a new registry. You may want to use multiple registries if you want to - /// export different sets of metrics via different exporters (e.g. on different URLs). - /// - public static CollectorRegistry NewCustomRegistry() => new CollectorRegistry(); - - /// - /// Returns an instance of that you can use to register metrics in a custom registry. - /// - public static MetricFactory WithCustomRegistry(CollectorRegistry registry) => - new MetricFactory(registry); - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public static Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) => - _defaultFactory.CreateCounter(name, help, configuration); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public static Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) => - _defaultFactory.CreateGauge(name, help, configuration); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public static Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) => - _defaultFactory.CreateSummary(name, help, configuration); - - /// - /// Histograms track the size and number of events in buckets. - /// - public static Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) => - _defaultFactory.CreateHistogram(name, help, configuration); - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public static Counter CreateCounter(string name, string help, params string[] labelNames) => - _defaultFactory.CreateCounter(name, help, labelNames); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public static Gauge CreateGauge(string name, string help, params string[] labelNames) => - _defaultFactory.CreateGauge(name, help, labelNames); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public static Summary CreateSummary(string name, string help, params string[] labelNames) => - _defaultFactory.CreateSummary(name, help, labelNames); - - /// - /// Histograms track the size and number of events in buckets. - /// - public static Histogram CreateHistogram(string name, string help, params string[] labelNames) => - _defaultFactory.CreateHistogram(name, help, labelNames); - - static Metrics() - { - DefaultRegistry = new CollectorRegistry(); - DefaultRegistry.SetBeforeFirstCollectCallback(delegate - { - // We include some metrics by default, just to give some output when a user first uses the library. - // These are not designed to be super meaningful/useful metrics. - DotNetStats.Register(DefaultRegistry); - }); - - _defaultFactory = new MetricFactory(DefaultRegistry); - } - - /// - /// Suppresses the registration of the default sample metrics from the default registry. - /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). - /// - public static void SuppressDefaultMetrics() - { - // Only has effect if called before the registry is collected from. - DefaultRegistry.SetBeforeFirstCollectCallback(delegate { }); - } - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/Prometheus.NetStandard.csproj b/Prometheus.NetStandard/Prometheus.NetStandard.csproj deleted file mode 100644 index d7708cac..00000000 --- a/Prometheus.NetStandard/Prometheus.NetStandard.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - netstandard2.0 - - - net461;netstandard2.0 - - - - Prometheus - - false - - true - - ..\Resources\prometheus-net.snk - - preview - enable - - true - - true - true - 1701;1702;1705;1591 - - - - - - - - - - - - - - - - diff --git a/Prometheus.NetStandard/PrometheusConstants.cs b/Prometheus.NetStandard/PrometheusConstants.cs deleted file mode 100644 index 76768197..00000000 --- a/Prometheus.NetStandard/PrometheusConstants.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text; - -namespace Prometheus -{ - public static class PrometheusConstants - { - public const string ExporterContentType = "text/plain; version=0.0.4; charset=utf-8"; - - // ASP.NET does not want to accept the parameters in PushStreamContent for whatever reason... - public const string ExporterContentTypeMinimal = "text/plain"; - - // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never - // pre-pended to the output stream. - public static readonly Encoding ExportEncoding = new UTF8Encoding(false); - } -} diff --git a/Prometheus.NetStandard/PushStreamContentInternal.cs b/Prometheus.NetStandard/PushStreamContentInternal.cs deleted file mode 100644 index e5489d2f..00000000 --- a/Prometheus.NetStandard/PushStreamContentInternal.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Simplified for prometheus-net for dependency reduction reasons. - -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.IO; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -namespace System.Net.Http -{ - /// - /// Provides an implementation that exposes an output - /// which can be written to directly. The ability to push data to the output stream differs from the - /// where data is pulled and not pushed. - /// - sealed class PushStreamContentInternal : HttpContent - { - private readonly Func _onStreamAvailable; - - private static readonly MediaTypeHeaderValue OctetStreamHeaderValue = MediaTypeHeaderValue.Parse("application/octet-stream"); - - /// - /// Initializes a new instance of the class with the given . - /// - public PushStreamContentInternal(Func onStreamAvailable, MediaTypeHeaderValue mediaType) - { - _onStreamAvailable = onStreamAvailable; - Headers.ContentType = mediaType ?? OctetStreamHeaderValue; - } - - /// - /// When this method is called, it calls the action provided in the constructor with the output - /// stream to write to. Once the action has completed its work it closes the stream which will - /// close this content instance and complete the HTTP request or response. - /// - /// The to which to write. - /// The associated . - /// A instance that is asynchronously serializing the object's content. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")] - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - TaskCompletionSource serializeToStreamTask = new TaskCompletionSource(); - - Stream wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); - await _onStreamAvailable(wrappedStream, this, context); - - // wait for wrappedStream.Close/Dispose to get called. - await serializeToStreamTask.Task; - } - - /// - /// Computes the length of the stream if possible. - /// - /// The computed length of the stream. - /// true if the length has been computed; otherwise false. - protected override bool TryComputeLength(out long length) - { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; - } - - internal class CompleteTaskOnCloseStream : DelegatingStreamInternal - { - private TaskCompletionSource _serializeToStreamTask; - - public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask) - : base(innerStream) - { - _serializeToStreamTask = serializeToStreamTask; - } - - [SuppressMessage( - "Microsoft.Usage", - "CA2215:Dispose methods should call base class dispose", - Justification = "See comments, this is intentional.")] - protected override void Dispose(bool disposing) - { - // We don't dispose the underlying stream because we don't own it. Dispose in this case just signifies - // that the user's action is finished. - _serializeToStreamTask.TrySetResult(true); - } - } - } -} diff --git a/Prometheus.NetStandard/QuantileEpsilonPair.cs b/Prometheus.NetStandard/QuantileEpsilonPair.cs deleted file mode 100644 index 1ccf40a7..00000000 --- a/Prometheus.NetStandard/QuantileEpsilonPair.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Prometheus -{ - public readonly struct QuantileEpsilonPair - { - public QuantileEpsilonPair(double quantile, double epsilon) - { - Quantile = quantile; - Epsilon = epsilon; - } - - public double Quantile { get; } - public double Epsilon { get; } - } -} diff --git a/Prometheus.NetStandard/ScrapeFailedException.cs b/Prometheus.NetStandard/ScrapeFailedException.cs deleted file mode 100644 index 41c2b19c..00000000 --- a/Prometheus.NetStandard/ScrapeFailedException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Prometheus -{ - /// - /// Signals to the metrics server that metrics are currently unavailable. Thrown from "before collect" callbacks. - /// This causes the entire export operation to fail - even if some metrics are available, they will not be exported. - /// - /// The exception message will be delivered as the HTTP response body by the exporter. - /// - [Serializable] - public class ScrapeFailedException : Exception - { - public ScrapeFailedException() { } - public ScrapeFailedException(string message) : base(message) { } - public ScrapeFailedException(string message, Exception inner) : base(message, inner) { } - protected ScrapeFailedException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Prometheus.NetStandard/Summary.cs b/Prometheus.NetStandard/Summary.cs deleted file mode 100644 index 50e65707..00000000 --- a/Prometheus.NetStandard/Summary.cs +++ /dev/null @@ -1,313 +0,0 @@ -using Prometheus.SummaryImpl; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - public sealed class Summary : Collector, ISummary - { - // Label that defines the quantile in a summary. - private const string QuantileLabel = "quantile"; - - /// - /// Client library guidelines say that the summary should default to not measuring quantiles. - /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary - /// - internal static readonly QuantileEpsilonPair[] DefObjectivesArray = new QuantileEpsilonPair[0]; - - // Default Summary quantile values. - public static readonly IList DefObjectives = new List(DefObjectivesArray); - - // Default duration for which observations stay relevant - public static readonly TimeSpan DefMaxAge = TimeSpan.FromMinutes(10); - - // Default number of buckets used to calculate the age of observations - public static readonly int DefAgeBuckets = 5; - - // Standard buffer size for collecting Summary observations - public static readonly int DefBufCap = 500; - - private readonly IReadOnlyList _objectives; - private readonly TimeSpan _maxAge; - private readonly int _ageBuckets; - private readonly int _bufCap; - - internal Summary( - string name, - string help, - string[]? labelNames, - Labels staticLabels, - bool suppressInitialValue = false, - IReadOnlyList? objectives = null, - TimeSpan? maxAge = null, - int? ageBuckets = null, - int? bufCap = null) - : base(name, help, labelNames, staticLabels, suppressInitialValue) - { - _objectives = objectives ?? DefObjectivesArray; - _maxAge = maxAge ?? DefMaxAge; - _ageBuckets = ageBuckets ?? DefAgeBuckets; - _bufCap = bufCap ?? DefBufCap; - - if (_objectives.Count == 0) - _objectives = DefObjectivesArray; - - if (_maxAge < TimeSpan.Zero) - throw new ArgumentException($"Illegal max age {_maxAge}"); - - if (_ageBuckets == 0) - _ageBuckets = DefAgeBuckets; - - if (_bufCap == 0) - _bufCap = DefBufCap; - - if (labelNames?.Any(_ => _ == QuantileLabel) == true) - throw new ArgumentException($"{QuantileLabel} is a reserved label name"); - } - - private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish) - { - return new Child(this, labels, flattenedLabels, publish); - } - - private protected override MetricType Type => MetricType.Summary; - - public sealed class Child : ChildBase, ISummary - { - internal Child(Summary parent, Labels labels, Labels flattenedLabels, bool publish) - : base(parent, labels, flattenedLabels, publish) - { - _objectives = parent._objectives; - _maxAge = parent._maxAge; - _ageBuckets = parent._ageBuckets; - _bufCap = parent._bufCap; - - _sortedObjectives = new double[_objectives.Count]; - _hotBuf = new SampleBuffer(_bufCap); - _coldBuf = new SampleBuffer(_bufCap); - _streamDuration = new TimeSpan(_maxAge.Ticks / _ageBuckets); - _headStreamExpTime = DateTime.UtcNow.Add(_streamDuration); - _hotBufExpTime = _headStreamExpTime; - - _streams = new QuantileStream[_ageBuckets]; - for (var i = 0; i < _ageBuckets; i++) - { - _streams[i] = QuantileStream.NewTargeted(_objectives); - } - - _headStream = _streams[0]; - - for (var i = 0; i < _objectives.Count; i++) - { - _sortedObjectives[i] = _objectives[i].Quantile; - } - - Array.Sort(_sortedObjectives); - - _sumIdentifier = CreateIdentifier("sum"); - _countIdentifier = CreateIdentifier("count"); - - _quantileIdentifiers = new byte[_objectives.Count][]; - for (var i = 0; i < _objectives.Count; i++) - { - var value = double.IsPositiveInfinity(_objectives[i].Quantile) ? "+Inf" : _objectives[i].Quantile.ToString(CultureInfo.InvariantCulture); - - _quantileIdentifiers[i] = CreateIdentifier(null, ("quantile", value)); - } - } - - private readonly byte[] _sumIdentifier; - private readonly byte[] _countIdentifier; - private readonly byte[][] _quantileIdentifiers; - - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - // We output sum. - // We output count. - // We output quantiles. - - var now = DateTime.UtcNow; - - double count; - double sum; - var values = new List<(double quantile, double value)>(_objectives.Count); - - lock (_bufLock) - { - lock (_lock) - { - // Swap bufs even if hotBuf is empty to set new hotBufExpTime. - SwapBufs(now); - FlushColdBuf(); - - count = _count; - sum = _sum; - - for (var i = 0; i < _sortedObjectives.Length; i++) - { - var quantile = _sortedObjectives[i]; - var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile); - - values.Add((quantile, value)); - } - } - } - - await serializer.WriteMetricAsync(_sumIdentifier, sum, cancel); - await serializer.WriteMetricAsync(_countIdentifier, count, cancel); - - for (var i = 0; i < values.Count; i++) - await serializer.WriteMetricAsync(_quantileIdentifiers[i], values[i].value, cancel); - } - - // Objectives defines the quantile rank estimates with their respective - // absolute error. If Objectives[q] = e, then the value reported - // for q will be the φ-quantile value for some φ between q-e and q+e. - // The default value is DefObjectives. - private IReadOnlyList _objectives = new List(); - private double[] _sortedObjectives; - private double _sum; - private ulong _count; - private SampleBuffer _hotBuf; - private SampleBuffer _coldBuf; - private QuantileStream[] _streams; - private TimeSpan _streamDuration; - private QuantileStream _headStream; - private int _headStreamIdx; - private DateTime _headStreamExpTime; - private DateTime _hotBufExpTime; - - // Protects hotBuf and hotBufExpTime. - private readonly object _bufLock = new object(); - - // Protects every other moving part. - // Lock bufMtx before mtx if both are needed. - private readonly object _lock = new object(); - - // MaxAge defines the duration for which an observation stays relevant - // for the summary. Must be positive. The default value is DefMaxAge. - private TimeSpan _maxAge; - - // AgeBuckets is the number of buckets used to exclude observations that - // are older than MaxAge from the summary. A higher number has a - // resource penalty, so only increase it if the higher resolution is - // really required. For very high observation rates, you might want to - // reduce the number of age buckets. With only one age bucket, you will - // effectively see a complete reset of the summary each time MaxAge has - // passed. The default value is DefAgeBuckets. - private int _ageBuckets; - - // BufCap defines the default sample stream buffer size. The default - // value of DefBufCap should suffice for most uses. If there is a need - // to increase the value, a multiple of 500 is recommended (because that - // is the internal buffer size of the underlying package - // "github.com/bmizerany/perks/quantile"). - private int _bufCap; - - public void Observe(double val) - { - Observe(val, DateTime.UtcNow); - } - - /// - /// For unit tests only - /// - internal void Observe(double val, DateTime now) - { - if (double.IsNaN(val)) - return; - - lock (_bufLock) - { - if (now > _hotBufExpTime) - Flush(now); - - _hotBuf.Append(val); - - if (_hotBuf.IsFull) - Flush(now); - } - - Publish(); - } - - // Flush needs bufMtx locked. - private void Flush(DateTime now) - { - lock (_lock) - { - SwapBufs(now); - - // Go version flushes on a separate goroutine, but doing this on another - // thread actually makes the benchmark tests slower in .net - FlushColdBuf(); - } - } - - // SwapBufs needs mtx AND bufMtx locked, coldBuf must be empty. - private void SwapBufs(DateTime now) - { - if (!_coldBuf.IsEmpty) - throw new InvalidOperationException("coldBuf is not empty"); - - var temp = _hotBuf; - _hotBuf = _coldBuf; - _coldBuf = temp; - - // hotBuf is now empty and gets new expiration set. - while (now > _hotBufExpTime) - { - _hotBufExpTime = _hotBufExpTime.Add(_streamDuration); - } - } - - // FlushColdBuf needs mtx locked. - private void FlushColdBuf() - { - for (var bufIdx = 0; bufIdx < _coldBuf.Position; bufIdx++) - { - var value = _coldBuf[bufIdx]; - - for (var streamIdx = 0; streamIdx < _streams.Length; streamIdx++) - { - _streams[streamIdx].Insert(value); - } - - _count++; - _sum += value; - } - - _coldBuf.Reset(); - MaybeRotateStreams(); - } - - // MaybeRotateStreams needs mtx AND bufMtx locked. - private void MaybeRotateStreams() - { - while (!_hotBufExpTime.Equals(_headStreamExpTime)) - { - _headStream.Reset(); - _headStreamIdx++; - - if (_headStreamIdx >= _streams.Length) - _headStreamIdx = 0; - - _headStream = _streams[_headStreamIdx]; - _headStreamExpTime = _headStreamExpTime.Add(_streamDuration); - } - } - } - - public void Observe(double val) - { - Unlabelled.Observe(val); - } - - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/SummaryConfiguration.cs b/Prometheus.NetStandard/SummaryConfiguration.cs deleted file mode 100644 index 5f20c9d0..00000000 --- a/Prometheus.NetStandard/SummaryConfiguration.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Prometheus -{ - public sealed class SummaryConfiguration : MetricConfiguration - { - internal static readonly SummaryConfiguration Default = new SummaryConfiguration(); - - /// - /// Pairs of quantiles and allowed error values (epsilon). - /// - /// For example, a quantile of 0.95 with an epsilon of 0.01 means the calculated value - /// will be between the 94th and 96th quantile. - /// - /// If null, no quantiles will be calculated! - /// - public IReadOnlyList Objectives { get; set; } = Summary.DefObjectivesArray; - - /// - /// Time span over which to calculate the summary. - /// - public TimeSpan MaxAge { get; set; } = Summary.DefMaxAge; - - /// - /// Number of buckets used to control measurement expiration. - /// - public int AgeBuckets { get; set; } = Summary.DefAgeBuckets; - - /// - /// Buffer size limit. Use multiples of 500 to avoid waste, as internal buffers use that size. - /// - public int BufferSize { get; set; } = Summary.DefBufCap; - } -} diff --git a/Prometheus.NetStandard/SummaryImpl/QuantileStream.cs b/Prometheus.NetStandard/SummaryImpl/QuantileStream.cs deleted file mode 100644 index 25769b07..00000000 --- a/Prometheus.NetStandard/SummaryImpl/QuantileStream.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Collections.Generic; - -namespace Prometheus.SummaryImpl -{ - // Ported from https://github.com/beorn7/perks/blob/master/quantile/stream.go - - // Package quantile computes approximate quantiles over an unbounded data - // stream within low memory and CPU bounds. - // - // A small amount of accuracy is traded to achieve the above properties. - // - // Multiple streams can be merged before calling Query to generate a single set - // of results. This is meaningful when the streams represent the same type of - // data. See Merge and Samples. - // - // For more detailed information about the algorithm used, see: - // - // Effective Computation of Biased Quantiles over Data Streams - // - // http://www.cs.rutgers.edu/~muthu/bquant.pdf - - internal delegate double Invariant(SampleStream stream, double r); - - internal class QuantileStream - { - private readonly SampleStream _sampleStream; - private readonly List _samples; - private bool _sorted; - - private QuantileStream(SampleStream sampleStream, List samples, bool sorted) - { - _sampleStream = sampleStream; - _samples = samples; - _sorted = sorted; - } - - public static QuantileStream NewStream(Invariant invariant) - { - return new QuantileStream(new SampleStream(invariant), new List { Capacity = 500 }, true); - } - - // NewLowBiased returns an initialized Stream for low-biased quantiles - // (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but - // error guarantees can still be given even for the lower ranks of the data - // distribution. - // - // The provided epsilon is a relative error, i.e. the true quantile of a value - // returned by a query is guaranteed to be within (1±Epsilon)*Quantile. - // - // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error - // properties. - public static QuantileStream NewLowBiased(double epsilon) - { - return NewStream((stream, r) => 2 * epsilon * r); - } - - // NewHighBiased returns an initialized Stream for high-biased quantiles - // (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but - // error guarantees can still be given even for the higher ranks of the data - // distribution. - // - // The provided epsilon is a relative error, i.e. the true quantile of a value - // returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile). - // - // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error - // properties. - public static QuantileStream NewHighBiased(double epsilon) - { - return NewStream((stream, r) => 2 * epsilon * (stream.N - r)); - } - - // NewTargeted returns an initialized Stream concerned with a particular set of - // quantile values that are supplied a priori. Knowing these a priori reduces - // space and computation time. The targets map maps the desired quantiles to - // their absolute errors, i.e. the true quantile of a value returned by a query - // is guaranteed to be within (Quantile±Epsilon). - // - // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. - public static QuantileStream NewTargeted(IReadOnlyList targets) - { - return NewStream((stream, r) => - { - var m = double.MaxValue; - - for (var i = 0; i < targets.Count; i++) - { - var target = targets[i]; - - double f; - if (target.Quantile * stream.N <= r) - f = (2 * target.Epsilon * r) / target.Quantile; - else - f = (2 * target.Epsilon * (stream.N - r)) / (1 - target.Quantile); - - if (f < m) - m = f; - } - - return m; - }); - } - - public void Insert(double value) - { - Insert(new Sample { Value = value, Width = 1 }); - } - - private void Insert(Sample sample) - { - _samples.Add(sample); - _sorted = false; - if (_samples.Count == _samples.Capacity) - Flush(); - } - - private void Flush() - { - MaybeSort(); - _sampleStream.Merge(_samples); - _samples.Clear(); - } - - private void MaybeSort() - { - if (!_sorted) - { - _sorted = true; - _samples.Sort(SampleComparison); - } - } - - private static int SampleComparison(Sample lhs, Sample rhs) - { - return lhs.Value.CompareTo(rhs.Value); - } - - public void Reset() - { - _sampleStream.Reset(); - _samples.Clear(); - } - - // Count returns the total number of samples observed in the stream since initialization. - public int Count => _samples.Count + _sampleStream.Count; - - public int SamplesCount => _samples.Count; - - public bool Flushed => _sampleStream.SampleCount > 0; - - // Query returns the computed qth percentiles value. If s was created with - // NewTargeted, and q is not in the set of quantiles provided a priori, Query - // will return an unspecified result. - public double Query(double q) - { - if (!Flushed) - { - // Fast path when there hasn't been enough data for a flush; - // this also yields better accuracy for small sets of data. - - var l = _samples.Count; - - if (l == 0) - return 0; - - var i = (int)(l * q); - if (i > 0) - i -= 1; - - MaybeSort(); - return _samples[i].Value; - } - - Flush(); - return _sampleStream.Query(q); - } - } -} diff --git a/Prometheus.NetStandard/SummaryImpl/SampleBuffer.cs b/Prometheus.NetStandard/SummaryImpl/SampleBuffer.cs deleted file mode 100644 index 2347640f..00000000 --- a/Prometheus.NetStandard/SummaryImpl/SampleBuffer.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; - -namespace Prometheus.SummaryImpl -{ - internal class SampleBuffer - { - private readonly double[] _buffer; - - public SampleBuffer(int capacity) - { - if (capacity <= 0) - throw new ArgumentOutOfRangeException(nameof(capacity), "Must be > 0"); - - _buffer = new double[capacity]; - Position = 0; - } - - public void Append(double value) - { - if (Position >= Capacity) - throw new InvalidOperationException("Buffer is full"); - - _buffer[Position++] = value; - } - - public double this[int index] - { - get - { - if (index > Position) - throw new ArgumentOutOfRangeException(nameof(index), "Index is greater than position"); - - return _buffer[index]; - } - } - - public void Reset() - { - Position = 0; - } - - public int Position { get; private set; } - - public int Capacity => _buffer.Length; - public bool IsFull => Position == Capacity; - public bool IsEmpty => Position == 0; - } -} diff --git a/Prometheus.NetStandard/SummaryImpl/SampleStream.cs b/Prometheus.NetStandard/SummaryImpl/SampleStream.cs deleted file mode 100644 index 2783f5ab..00000000 --- a/Prometheus.NetStandard/SummaryImpl/SampleStream.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Prometheus.SummaryImpl -{ - internal class SampleStream - { - public double N; - private readonly List _samples = new List(); - private readonly Invariant _invariant; - - public SampleStream(Invariant invariant) - { - _invariant = invariant; - } - - public void Merge(List samples) - { - // TODO(beorn7): This tries to merge not only individual samples, but - // whole summaries. The paper doesn't mention merging summaries at - // all. Unittests show that the merging is inaccurate. Find out how to - // do merges properly. - - double r = 0; - var i = 0; - - for (var sampleIdx = 0; sampleIdx < samples.Count; sampleIdx++) - { - var sample = samples[sampleIdx]; - - for (; i < _samples.Count; i++) - { - var c = _samples[i]; - - if (c.Value > sample.Value) - { - // Insert at position i - _samples.Insert(i, new Sample { Value = sample.Value, Width = sample.Width, Delta = Math.Max(sample.Delta, Math.Floor(_invariant(this, r)) - 1) }); - i++; - goto inserted; - } - r += c.Width; - } - _samples.Add(new Sample { Value = sample.Value, Width = sample.Width, Delta = 0 }); - i++; - - inserted: - N += sample.Width; - r += sample.Width; - } - - Compress(); - } - - private void Compress() - { - if (_samples.Count < 2) - return; - - var x = _samples[_samples.Count - 1]; - var xi = _samples.Count - 1; - var r = N - 1 - x.Width; - - for (var i = _samples.Count - 2; i >= 0; i--) - { - var c = _samples[i]; - - if (c.Width + x.Width + x.Delta <= _invariant(this, r)) - { - x.Width += c.Width; - _samples[xi] = x; - _samples.RemoveAt(i); - xi -= 1; - } - else - { - x = c; - xi = i; - } - - r -= c.Width; - } - } - - public void Reset() - { - _samples.Clear(); - N = 0; - } - - public int Count => (int)N; - - public double Query(double q) - { - var t = Math.Ceiling(q * N); - t += Math.Ceiling(_invariant(this, t) / 2); - var p = _samples[0]; - double r = 0; - - for (var i = 1; i < _samples.Count; i++) - { - var c = _samples[i]; - r += p.Width; - - if (r + c.Width + c.Delta > t) - return p.Value; - - p = c; - } - - return p.Value; - } - - public int SampleCount => _samples.Count; - } -} \ No newline at end of file diff --git a/Prometheus.NetStandard/TextSerializer.cs b/Prometheus.NetStandard/TextSerializer.cs deleted file mode 100644 index 7b62ed18..00000000 --- a/Prometheus.NetStandard/TextSerializer.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Prometheus -{ - /// - /// Does NOT take ownership of the stream - caller remains the boss. - /// - internal sealed class TextSerializer : IMetricsSerializer - { - private static readonly byte[] NewLine = new[] { (byte)'\n' }; - private static readonly byte[] Space = new[] { (byte)' ' }; - - public TextSerializer(Stream stream) - { - _stream = new Lazy(() => stream); - } - - // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. - public TextSerializer(Func streamFactory) - { - _stream = new Lazy(streamFactory); - } - - public async Task FlushAsync(CancellationToken cancel) - { - // If we never opened the stream, we don't touch it on flush. - if (!_stream.IsValueCreated) - return; - - await _stream.Value.FlushAsync(cancel); - } - - private readonly Lazy _stream; - - // # HELP name help - // # TYPE name type - public async Task WriteFamilyDeclarationAsync(byte[][] headerLines, CancellationToken cancel) - { - foreach (var line in headerLines) - { - await _stream.Value.WriteAsync(line, 0, line.Length, cancel); - await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); - } - } - - // Reuse a buffer to do the UTF-8 encoding. - // Maybe one day also ValueStringBuilder but that would be .NET Core only. - // https://github.com/dotnet/corefx/issues/28379 - // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd - private readonly byte[] _stringBytesBuffer = new byte[32]; - - // name{labelkey1="labelvalue1",labelkey2="labelvalue2"} 123.456 - public async Task WriteMetricAsync(byte[] identifier, double value, CancellationToken cancel) - { - await _stream.Value.WriteAsync(identifier, 0, identifier.Length, cancel); - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); - - var valueAsString = value.ToString(CultureInfo.InvariantCulture); - - var numBytes = PrometheusConstants.ExportEncoding - .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); - - await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); - await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); - } - } -} diff --git a/Prometheus.NetStandard/ThreadSafeDouble.cs b/Prometheus.NetStandard/ThreadSafeDouble.cs deleted file mode 100644 index 72858710..00000000 --- a/Prometheus.NetStandard/ThreadSafeDouble.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Globalization; -using System.Threading; - -namespace Prometheus -{ - internal struct ThreadSafeDouble - { - private long _value; - - public ThreadSafeDouble(double value) - { - _value = BitConverter.DoubleToInt64Bits(value); - } - - public double Value - { - get - { - return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _value)); - } - set - { - Interlocked.Exchange(ref _value, BitConverter.DoubleToInt64Bits(value)); - } - } - - public void Add(double increment) - { - while (true) - { - long initialValue = _value; - double computedValue = BitConverter.Int64BitsToDouble(initialValue) + increment; - - if (initialValue == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(computedValue), initialValue)) - return; - } - } - - /// - /// Sets the value to this, unless the existing value is already greater. - /// - public void IncrementTo(double to) - { - while (true) - { - long initialRaw = _value; - double initialValue = BitConverter.Int64BitsToDouble(initialRaw); - - if (initialValue >= to) - return; // Already greater. - - if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) - return; - } - } - - /// - /// Sets the value to this, unless the existing value is already smaller. - /// - public void DecrementTo(double to) - { - while (true) - { - long initialRaw = _value; - double initialValue = BitConverter.Int64BitsToDouble(initialRaw); - - if (initialValue <= to) - return; // Already greater. - - if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) - return; - } - } - - public override string ToString() - { - return Value.ToString(CultureInfo.InvariantCulture); - } - - public override bool Equals(object obj) - { - if (obj is ThreadSafeDouble) - return Value.Equals(((ThreadSafeDouble)obj).Value); - - return Value.Equals(obj); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - } -} diff --git a/Prometheus.NetStandard/ThreadSafeLong.cs b/Prometheus.NetStandard/ThreadSafeLong.cs deleted file mode 100644 index aecd67d4..00000000 --- a/Prometheus.NetStandard/ThreadSafeLong.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Globalization; -using System.Threading; - -namespace Prometheus -{ - internal struct ThreadSafeLong - { - private long _value; - - public ThreadSafeLong(long value) - { - _value = value; - } - - public long Value - { - get - { - return Interlocked.Read(ref _value); - } - set - { - Interlocked.Exchange(ref _value, value); - } - } - - public void Add(long increment) - { - Interlocked.Add(ref _value, increment); - } - - public override string ToString() - { - return Value.ToString(CultureInfo.InvariantCulture); - } - - public override bool Equals(object obj) - { - if (obj is ThreadSafeLong) - return Value.Equals(((ThreadSafeLong)obj).Value); - - return Value.Equals(obj); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - } -} diff --git a/Prometheus.NetStandard/TimerExtensions.cs b/Prometheus.NetStandard/TimerExtensions.cs deleted file mode 100644 index bbb520fa..00000000 --- a/Prometheus.NetStandard/TimerExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; - -namespace Prometheus -{ - public static class TimerExtensions - { - private sealed class Timer : ITimer - { - private readonly ValueStopwatch _stopwatch = ValueStopwatch.StartNew(); - private readonly Action _observeDurationAction; - - public Timer(IObserver observer) - { - _observeDurationAction = duration => observer.Observe(duration); - } - - public Timer(IGauge gauge) - { - _observeDurationAction = duration => gauge.Set(duration); - } - - public Timer(ICounter counter) - { - _observeDurationAction = duration => counter.Inc(duration); - } - - public TimeSpan ObserveDuration() - { - var duration = _stopwatch.GetElapsedTime(); - _observeDurationAction.Invoke(duration.TotalSeconds); - - return duration; - } - - public void Dispose() - { - ObserveDuration(); - } - } - - /// - /// Enables you to easily report elapsed seconds in the value of an observer. - /// Dispose of the returned instance to report the elapsed duration. - /// - public static ITimer NewTimer(this IObserver observer) - { - return new Timer(observer); - } - - /// - /// Enables you to easily report elapsed seconds in the value of a gauge. - /// Dispose of the returned instance to report the elapsed duration. - /// - public static ITimer NewTimer(this IGauge gauge) - { - return new Timer(gauge); - } - - /// - /// Enables you to easily report elapsed seconds in the value of a counter. - /// The duration (in seconds) will be added to the value of the counter. - /// Dispose of the returned instance to report the elapsed duration. - /// - public static ITimer NewTimer(this ICounter counter) - { - return new Timer(counter); - } - } -} diff --git a/Prometheus.NetStandard/TimestampHelpers.cs b/Prometheus.NetStandard/TimestampHelpers.cs deleted file mode 100644 index 1d97da91..00000000 --- a/Prometheus.NetStandard/TimestampHelpers.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace Prometheus -{ - static class TimestampHelpers - { - // Math copypasted from DateTimeOffset.cs in .NET Framework. - - // Number of days in a non-leap year - private const int DaysPerYear = 365; - // Number of days in 4 years - private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 - // Number of days in 100 years - private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 - // Number of days in 400 years - private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 - private const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162 - private const long UnixEpochTicks = TimeSpan.TicksPerDay * DaysTo1970; // 621,355,968,000,000,000 - private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 - - public static double ToUnixTimeSecondsAsDouble(DateTimeOffset timestamp) - { - // This gets us sub-millisecond precision, which is better than ToUnixTimeMilliseconds(). - var ticksSinceUnixEpoch = timestamp.ToUniversalTime().Ticks - UnixEpochSeconds * TimeSpan.TicksPerSecond; - return ticksSinceUnixEpoch / (double)TimeSpan.TicksPerSecond; - } - } -} diff --git a/Prometheus.NetStandard/ValueStopwatch.cs b/Prometheus.NetStandard/ValueStopwatch.cs deleted file mode 100644 index 47b8a736..00000000 --- a/Prometheus.NetStandard/ValueStopwatch.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Diagnostics; - -// Copied from: https://github.com/dotnet/extensions/blob/master/src/Shared/src/ValueStopwatch/ValueStopwatch.cs -namespace Prometheus -{ - internal struct ValueStopwatch - { - private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - - private long _startTimestamp; - - public bool IsActive => _startTimestamp != 0; - - private ValueStopwatch(long startTimestamp) - { - _startTimestamp = startTimestamp; - } - - public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); - - public TimeSpan GetElapsedTime() - { - // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0. - // So it being 0 is a clear indication of default(ValueStopwatch) - if (!IsActive) - { - throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); - } - - var end = Stopwatch.GetTimestamp(); - var timestampDelta = end - _startTimestamp; - var ticks = (long)(TimestampToTicks * timestampDelta); - return new TimeSpan(ticks); - } - } -} diff --git a/Prometheus/CanonicalLabel.cs b/Prometheus/CanonicalLabel.cs new file mode 100644 index 00000000..96fc4ceb --- /dev/null +++ b/Prometheus/CanonicalLabel.cs @@ -0,0 +1,13 @@ +namespace Prometheus; + +internal readonly struct CanonicalLabel(byte[] name, byte[] prometheus, byte[] openMetrics) +{ + public static readonly CanonicalLabel Empty = new([], [], []); + + public byte[] Name { get; } = name; + + public byte[] Prometheus { get; } = prometheus; + public byte[] OpenMetrics { get; } = openMetrics; + + public bool IsNotEmpty => Name.Length > 0; +} \ No newline at end of file diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs new file mode 100644 index 00000000..7e79b98b --- /dev/null +++ b/Prometheus/ChildBase.cs @@ -0,0 +1,195 @@ +namespace Prometheus; + +/// +/// Base class for labeled instances of metrics (with all label names and label values defined). +/// +public abstract class ChildBase : ICollectorChild, IDisposable +{ + internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + Parent = parent; + InstanceLabels = instanceLabels; + FlattenedLabels = flattenedLabels; + _publish = publish; + _exemplarBehavior = exemplarBehavior; + } + + private readonly ExemplarBehavior _exemplarBehavior; + + /// + /// Marks the metric as one to be published, even if it might otherwise be suppressed. + /// + /// This is useful for publishing zero-valued metrics once you have loaded data on startup and determined + /// that there is no need to increment the value of the metric. + /// + /// + /// Subclasses must call this when their value is first set, to mark the metric as published. + /// + public void Publish() + { + Volatile.Write(ref _publish, true); + } + + /// + /// Marks the metric as one to not be published. + /// + /// The metric will be published when Publish() is called or the value is updated. + /// + public void Unpublish() + { + Volatile.Write(ref _publish, false); + } + + /// + /// Removes this labeled instance from metrics. + /// It will no longer be published and any existing measurements/buckets will be discarded. + /// + public void Remove() + { + Parent.RemoveLabelled(InstanceLabels); + } + + public void Dispose() => Remove(); + + /// + /// Labels specific to this metric instance, without any inherited static labels. + /// Internal for testing purposes only. + /// + internal LabelSequence InstanceLabels { get; } + + /// + /// All labels that materialize on this metric instance, including inherited static labels. + /// Internal for testing purposes only. + /// + internal LabelSequence FlattenedLabels { get; } + + internal byte[] FlattenedLabelsBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _flattenedLabelsBytes, this, _assignFlattenedLabelsBytesFunc)!; + private byte[]? _flattenedLabelsBytes; + private static readonly Action _assignFlattenedLabelsBytesFunc = AssignFlattenedLabelsBytes; + private static void AssignFlattenedLabelsBytes(ChildBase instance) => instance._flattenedLabelsBytes = instance.FlattenedLabels.Serialize(); + + internal readonly Collector Parent; + + private bool _publish; + + /// + /// Collects all the metric data rows from this collector and serializes it using the given serializer. + /// + /// + /// Subclass must check _publish and suppress output if it is false. + /// + internal ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + if (!Volatile.Read(ref _publish)) + return default; + + return CollectAndSerializeImplAsync(serializer, cancel); + } + + // Same as above, just only called if we really need to serialize this metric (if publish is true). + private protected abstract ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); + + /// + /// Borrows an exemplar temporarily, to be later returned via ReturnBorrowedExemplar. + /// Borrowing ensures that no other thread is modifying it (as exemplars are not thread-safe). + /// You would typically want to do this while serializing the exemplar. + /// + internal static ObservedExemplar BorrowExemplar(ref ObservedExemplar storage) + { + return Interlocked.Exchange(ref storage, ObservedExemplar.Empty); + } + + /// + /// Returns a borrowed exemplar to storage or the object pool, with correct handling for cases where it is Empty. + /// + internal static void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemplar borrowed) + { + if (borrowed == ObservedExemplar.Empty) + return; + + // Return the exemplar unless a new one has arrived, in which case we discard the old one we were holding. + var foundExemplar = Interlocked.CompareExchange(ref storage, borrowed, ObservedExemplar.Empty); + + if (foundExemplar != ObservedExemplar.Empty) + { + // A new exemplar had already been written, so we could not return the borrowed one. That's perfectly fine - discard it. + ObservedExemplar.ReturnPooledIfNotEmpty(borrowed); + } + } + + internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, double observedValue) + { + exemplar.MarkAsConsumed(); + + // We do the "is allowed" check only if we really have an exemplar to record, to minimize the performance impact on users who do not use exemplars. + // If you are using exemplars, you are already paying for a lot of value serialization overhead, so this is insignificant. + // Whereas if you are not using exemplars, the difference from this simple check can be substantial. + if (!IsRecordingNewExemplarAllowed()) + { + // We will not record the exemplar but must still release the resources to the pool. + exemplar.ReturnToPoolIfNotEmpty(); + return; + } + + // ObservedExemplar takes ownership of the Exemplar and will return its resources to the pool when the time is right. + var observedExemplar = ObservedExemplar.CreatePooled(exemplar, observedValue); + ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref storage, observedExemplar)); + MarkNewExemplarHasBeenRecorded(); + + // We cannot record an exemplar every time we record an exemplar! + Volatile.Read(ref ExemplarsRecorded)?.Inc(Exemplar.None); + } + + protected Exemplar GetDefaultExemplar(double value) + { + if (_exemplarBehavior.DefaultExemplarProvider == null) + return Exemplar.None; + + return _exemplarBehavior.DefaultExemplarProvider(Parent, value); + } + + // May be replaced in test code. + internal static Func ExemplarRecordingTimestampProvider = DefaultExemplarRecordingTimestampProvider; + internal static double DefaultExemplarRecordingTimestampProvider() => LowGranularityTimeSource.GetSecondsFromUnixEpoch(); + + // Timetamp of when we last recorded an exemplar. We do not use ObservedExemplar.Timestamp because we do not want to + // read from an existing ObservedExemplar when we are writing to our metrics (to avoid the synchronization overhead). + // We start at a deep enough negative value to not cause funny behavior near zero point (only likely in tests, really). + private ThreadSafeDouble _exemplarLastRecordedTimestamp = new(-100_000_000); + + protected bool IsRecordingNewExemplarAllowed() + { + if (_exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero) + return true; + + var elapsedSeconds = ExemplarRecordingTimestampProvider() - _exemplarLastRecordedTimestamp.Value; + + return elapsedSeconds >= _exemplarBehavior.NewExemplarMinInterval.TotalSeconds; + } + + protected void MarkNewExemplarHasBeenRecorded() + { + if (_exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero) + return; // No need to record the timestamp if we are not enforcing a minimum interval. + + _exemplarLastRecordedTimestamp.Value = ExemplarRecordingTimestampProvider(); + } + + + // This is only set if and when debug metrics are enabled in the default registry. + private static Counter? ExemplarsRecorded; + + static ChildBase() + { + Metrics.DefaultRegistry.OnStartCollectingRegistryMetrics(delegate + { + Volatile.Write(ref ExemplarsRecorded, Metrics.CreateCounter("prometheus_net_exemplars_recorded_total", "Number of exemplars that were accepted into in-memory storage in the prometheus-net SDK.")); + }); + } + + public override string ToString() + { + // Just for debugging. + return $"{Parent.Name}{{{FlattenedLabels}}}"; + } +} \ No newline at end of file diff --git a/Prometheus/ChildLifetimeInfo.cs b/Prometheus/ChildLifetimeInfo.cs new file mode 100644 index 00000000..fa0e4e0e --- /dev/null +++ b/Prometheus/ChildLifetimeInfo.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; + +namespace Prometheus; + +/// +/// Describes a lifetime of a lifetime-managed metric instance. +/// +/// +/// Contents modified via atomic operations, not guarded by locks. +/// +internal sealed class ChildLifetimeInfo +{ + /// + /// Number of active leases. Nonzero value here indicates the lifetime extends forever. + /// + public int LeaseCount; + + /// + /// When the last lifetime related activity was performed. Expiration timer starts counting from here. + /// This is refreshed whenever a lease is released (a kept lease is a forever-keepalive, so we only care about releasing). + /// + public long KeepaliveTimestamp; + + /// + /// The lifetime has been ended, potentially while a lease was active. The next time a lease ends, + /// it will have to re-register the lifetime instead of just extending the existing one. + /// + public bool Ended; + + public override string ToString() + { + var leaseCount = Volatile.Read(ref LeaseCount); + var keepaliveTimestamp = Volatile.Read(ref KeepaliveTimestamp); + var ended = Volatile.Read(ref Ended); + + var age = PlatformCompatibilityHelpers.StopwatchGetElapsedTime(keepaliveTimestamp, Stopwatch.GetTimestamp()); + + return $"LeaseCount: {leaseCount}, KeepaliveTimestamp: {keepaliveTimestamp}, Ended: {ended}, Age: {age.TotalSeconds:F3} seconds"; + } +} \ No newline at end of file diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs new file mode 100644 index 00000000..45c45b6c --- /dev/null +++ b/Prometheus/Collector.cs @@ -0,0 +1,479 @@ +using System.Buffers; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.Extensions.ObjectPool; + +namespace Prometheus; + +/// +/// Base class for metrics, defining the basic informative API and the internal API. +/// +/// +/// Many of the fields are lazy-initialized to ensure we only perform the memory allocation if and when we actually use them. +/// For some, it means rarely used members are never allocated at all (e.g. if you never inspect the set of label names, they are never allocated). +/// For others, it means they are allocated at first time of use (e.g. serialization-related fields are allocated when serializing the first time). +/// +public abstract class Collector +{ + /// + /// The metric name, e.g. http_requests_total. + /// + public string Name { get; } + + internal byte[] NameBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _nameBytes, this, _assignNameBytesFunc)!; + private byte[]? _nameBytes; + private static readonly Action _assignNameBytesFunc = AssignNameBytes; + private static void AssignNameBytes(Collector instance) => instance._nameBytes = PrometheusConstants.ExportEncoding.GetBytes(instance.Name); + + /// + /// The help text describing the metric for a human audience. + /// + public string Help { get; } + + internal byte[] HelpBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _helpBytes, this, _assignHelpBytesFunc)!; + private byte[]? _helpBytes; + private static readonly Action _assignHelpBytesFunc = AssignHelpBytes; + private static void AssignHelpBytes(Collector instance) => + instance._helpBytes = string.IsNullOrWhiteSpace(instance.Help) ? [] : PrometheusConstants.ExportEncoding.GetBytes(instance.Help); + + /// + /// Names of the instance-specific labels (name-value pairs) that apply to this metric. + /// When the values are added to the names, you get a instance. + /// Does not include any static label names (from metric configuration, factory or registry). + /// + public string[] LabelNames => NonCapturingLazyInitializer.EnsureInitialized(ref _labelNames, this, _assignLabelNamesFunc)!; + private string[]? _labelNames; + private static readonly Action _assignLabelNamesFunc = AssignLabelNames; + private static void AssignLabelNames(Collector instance) => instance._labelNames = instance.InstanceLabelNames.ToArray(); + + internal StringSequence InstanceLabelNames; + internal StringSequence FlattenedLabelNames; + + /// + /// All static labels obtained from any hierarchy level (either defined in metric configuration or in registry). + /// These will be merged with the instance-specific labels to arrive at the final flattened label sequence for a specific child. + /// + internal LabelSequence StaticLabels; + + internal abstract MetricType Type { get; } + + internal byte[] TypeBytes { get; } + + internal abstract int ChildCount { get; } + internal abstract int TimeseriesCount { get; } + + internal abstract ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel); + + // Used by ChildBase.Remove() + internal abstract void RemoveLabelled(LabelSequence instanceLabels); + + private const string ValidMetricNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + private const string ValidLabelNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + private const string ReservedLabelNameExpression = "^__.*$"; + + private static readonly Regex MetricNameRegex = new(ValidMetricNameExpression, RegexOptions.Compiled); + private static readonly Regex LabelNameRegex = new(ValidLabelNameExpression, RegexOptions.Compiled); + private static readonly Regex ReservedLabelRegex = new(ReservedLabelNameExpression, RegexOptions.Compiled); + + internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels) + { + if (!MetricNameRegex.IsMatch(name)) + throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); + + Name = name; + TypeBytes = TextSerializer.MetricTypeToBytes[Type]; + Help = help; + InstanceLabelNames = instanceLabelNames; + StaticLabels = staticLabels; + + FlattenedLabelNames = instanceLabelNames.Concat(staticLabels.Names); + + // Used to check uniqueness of label names, to catch any label layering mistakes early. + var uniqueLabelNames = LabelValidationHashSetPool.Get(); + + try + { + foreach (var labelName in FlattenedLabelNames) + { + if (labelName == null) + throw new ArgumentException("One of the label names was null."); + + ValidateLabelName(labelName); + uniqueLabelNames.Add(labelName); + } + + // Here we check for label name collision, ensuring that the same label name is not defined twice on any label inheritance level. + if (uniqueLabelNames.Count != FlattenedLabelNames.Length) + throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", FlattenedLabelNames.ToArray())); + } + finally + { + LabelValidationHashSetPool.Return(uniqueLabelNames); + } + } + + private static readonly ObjectPool> LabelValidationHashSetPool = ObjectPool.Create(new LabelValidationHashSetPoolPolicy()); + + private sealed class LabelValidationHashSetPoolPolicy : PooledObjectPolicy> + { + // If something should explode the size, we do not return it to the pool. + // This should be more than generous even for the most verbosely labeled scenarios. + private const int PooledHashSetMaxSize = 50; + +#if NET + public override HashSet Create() => new(PooledHashSetMaxSize, StringComparer.Ordinal); +#else + public override HashSet Create() => new(StringComparer.Ordinal); +#endif + + public override bool Return(HashSet obj) + { + if (obj.Count > PooledHashSetMaxSize) + return false; + + obj.Clear(); + return true; + } + } + + internal static void ValidateLabelName(string labelName) + { + if (!LabelNameRegex.IsMatch(labelName)) + throw new ArgumentException($"Label name '{labelName}' does not match regex '{ValidLabelNameExpression}'."); + + if (ReservedLabelRegex.IsMatch(labelName)) + throw new ArgumentException($"Label name '{labelName}' is not valid - labels starting with double underscore are reserved!"); + } + + public override string ToString() + { + // Just for debugging. + return $"{Name}{{{FlattenedLabelNames}}}"; + } +} + +/// +/// Base class for metrics collectors, providing common labeled child management functionality. +/// +public abstract class Collector : Collector, ICollector + where TChild : ChildBase +{ + // Keyed by the instance labels (not by flattened labels!). + private readonly Dictionary _children = []; + private readonly ReaderWriterLockSlim _childrenLock = new(); + + // Lazy-initialized since not every collector will use a child with no labels. + // Lazy instance will be replaced if the unlabelled timeseries is removed. + private TChild? _lazyUnlabelled; + + /// + /// Gets the child instance that has no labels. + /// + protected internal TChild Unlabelled => LazyInitializer.EnsureInitialized(ref _lazyUnlabelled, _createdUnlabelledFunc)!; + + private TChild CreateUnlabelled() => GetOrAddLabelled(LabelSequence.Empty); + private readonly Func _createdUnlabelledFunc; + + // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured. + TChild ICollector.Unlabelled => Unlabelled; + + + // Old naming, deprecated for a silly reason: by default if you start typing .La... and trigger Intellisense + // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration. + // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience. + // Discourage this method as it can create confusion. But it works fine, so no reason to mark it obsolete, really. + [EditorBrowsable(EditorBrowsableState.Never)] + public TChild Labels(params string[] labelValues) => WithLabels(labelValues); + + public TChild WithLabels(params string[] labelValues) + { + if (labelValues == null) + throw new ArgumentNullException(nameof(labelValues)); + + return WithLabels(labelValues.AsMemory()); + } + + public TChild WithLabels(ReadOnlyMemory labelValues) + { + var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); + return GetOrAddLabelled(labels); + } + + public TChild WithLabels(ReadOnlySpan labelValues) + { + // We take ReadOnlySpan as a signal that the caller believes we may be able to perform the operation allocation-free because + // the label values are probably already known and a metric instance registered. There is no a guarantee, just a high probability. + // The implementation avoids allocating a long-lived string[] for the label values. We only allocate if we create a new instance. + + // We still need to process the label values as a reference type, so we transform the Span into a Memory using a pooled buffer. + var buffer = ArrayPool.Shared.Rent(labelValues.Length); + + try + { + labelValues.CopyTo(buffer); + + var temporaryLabels = LabelSequence.From(InstanceLabelNames, StringSequence.From(buffer.AsMemory(0, labelValues.Length))); + + if (TryGetLabelled(temporaryLabels, out var existing)) + return existing!; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + // If we got this far, we did not succeed in finding an existing instance. We need to allocate a long-lived string[] for the label values. + var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues.ToArray())); + return CreateLabelled(labels); + } + + public void RemoveLabelled(params string[] labelValues) + { + if (labelValues == null) + throw new ArgumentNullException(nameof(labelValues)); + + var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); + RemoveLabelled(labels); + } + + internal override void RemoveLabelled(LabelSequence labels) + { + _childrenLock.EnterWriteLock(); + + try + { + _children.Remove(labels); + + if (labels.Length == 0) + { + // If we remove the unlabeled instance (technically legitimate, if the caller really desires to do so) then + // we need to also ensure that the special-casing used for it gets properly wired up the next time. + Volatile.Write(ref _lazyUnlabelled, null); + } + } + finally + { + _childrenLock.ExitWriteLock(); + } + } + + internal override int ChildCount + { + get + { + _childrenLock.EnterReadLock(); + + try + { + return _children.Count; + } + finally + { + _childrenLock.ExitReadLock(); + } + } + } + + /// + /// Gets the instance-specific label values of all labelled instances of the collector. + /// Values of any inherited static labels are not returned in the result. + /// + /// Note that during concurrent operation, the set of values returned here + /// may diverge from the latest set of values used by the collector. + /// + public IEnumerable GetAllLabelValues() + { + // We are yielding here so make a defensive copy so we do not hold locks for a long time. + // We reuse this buffer, so it should be relatively harmless in the long run. + LabelSequence[] buffer; + + _childrenLock.EnterReadLock(); + + var childCount = _children.Count; + buffer = ArrayPool.Shared.Rent(childCount); + + try + { + try + { + _children.Keys.CopyTo(buffer, 0); + } + finally + { + _childrenLock.ExitReadLock(); + } + + for (var i = 0; i < childCount; i++) + { + var labels = buffer[i]; + + if (labels.Length == 0) + continue; // We do not return the "unlabelled" label set. + + // Defensive copy. + yield return labels.Values.ToArray(); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private TChild GetOrAddLabelled(LabelSequence instanceLabels) + { + // NOTE: We do not try to find a metric instance with the same set of label names but in a DIFFERENT order. + // Order of labels matters in data creation, although does not matter when the exported data set is imported later. + // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... + // That is not ideal but also not that big of a deal to justify a lookup every time a metric instance is registered. + + // First try to find an existing instance. This is the fast path, if we are re-looking-up an existing one. + if (TryGetLabelled(instanceLabels, out var existing)) + return existing!; + + // If no existing one found, grab the write lock and create a new one if needed. + return CreateLabelled(instanceLabels); + } + + private bool TryGetLabelled(LabelSequence instanceLabels, out TChild? child) + { + _childrenLock.EnterReadLock(); + + try + { + if (_children.TryGetValue(instanceLabels, out var existing)) + { + child = existing; + return true; + } + + child = null; + return false; + } + finally + { + _childrenLock.ExitReadLock(); + } + } + + private TChild CreateLabelled(LabelSequence instanceLabels) + { + var newChild = _createdLabelledChildFunc(instanceLabels); + + _childrenLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_children.TryAdd(instanceLabels, newChild)) + return newChild; + + return _children[instanceLabels]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_children.TryGetValue(instanceLabels, out var existing)) + return existing; + + _children.Add(instanceLabels, newChild); + return newChild; +#endif + } + finally + { + _childrenLock.ExitWriteLock(); + } + } + + private TChild CreateLabelledChild(LabelSequence instanceLabels) + { + // Order of labels is 1) instance labels; 2) static labels. + var flattenedLabels = instanceLabels.Concat(StaticLabels); + + return NewChild(instanceLabels, flattenedLabels, publish: !_suppressInitialValue, _exemplarBehavior); + } + + // Cache the delegate to avoid allocating a new one every time in GetOrAddLabelled. + private readonly Func _createdLabelledChildFunc; + + /// + /// For tests that want to see what instance-level label values were used when metrics were created. + /// This is for testing only, so does not respect locks - do not use this in concurrent context. + /// + internal LabelSequence[] GetAllInstanceLabelsUnsafe() => _children.Keys.ToArray(); + + internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels) + { + _createdUnlabelledFunc = CreateUnlabelled; + _createdLabelledChildFunc = CreateLabelledChild; + + _suppressInitialValue = suppressInitialValue; + _exemplarBehavior = exemplarBehavior; + } + + /// + /// Creates a new instance of the child collector type. + /// + private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior); + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + internal override async ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel) + { + EnsureUnlabelledMetricCreatedIfNoLabels(); + + // There may be multiple Collectors emitting data for the same family. Only the first will write out the family declaration. + if (writeFamilyDeclaration) + await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); + + // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). + // Therefore we operate on a defensive copy in a reused buffer. + TChild[] children; + + _childrenLock.EnterReadLock(); + + var childCount = _children.Count; + children = ArrayPool.Shared.Rent(childCount); + + try + { + try + { + _children.Values.CopyTo(children, 0); + } + finally + { + _childrenLock.ExitReadLock(); + } + + for (var i = 0; i < childCount; i++) + { + var child = children[i]; + await child.CollectAndSerializeAsync(serializer, cancel); + } + } + finally + { + ArrayPool.Shared.Return(children, clearArray: true); + } + } + + private readonly bool _suppressInitialValue; + + private void EnsureUnlabelledMetricCreatedIfNoLabels() + { + // We want metrics to exist even with 0 values if they are supposed to be used without labels. + // Labelled metrics are created when label values are assigned. However, as unlabelled metrics are lazy-created + // (they are optional if labels are used) we might lose them for cases where they really are desired. + + // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it. + // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default. + if (InstanceLabelNames.Length == 0) + LazyInitializer.EnsureInitialized(ref _lazyUnlabelled, _createdUnlabelledFunc); + } + + private readonly ExemplarBehavior _exemplarBehavior; +} \ No newline at end of file diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs new file mode 100644 index 00000000..6db89569 --- /dev/null +++ b/Prometheus/CollectorFamily.cs @@ -0,0 +1,179 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.ObjectPool; + +namespace Prometheus; + +internal sealed class CollectorFamily +{ + public Type CollectorType { get; } + + private readonly Dictionary _collectors = new(); + private readonly ReaderWriterLockSlim _lock = new(); + + public CollectorFamily(Type collectorType) + { + CollectorType = collectorType; + _collectAndSerializeFunc = CollectAndSerialize; + } + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + internal async ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + var operation = _serializeFamilyOperationPool.Get(); + operation.Serializer = serializer; + + await ForEachCollectorAsync(_collectAndSerializeFunc, operation, cancel); + + _serializeFamilyOperationPool.Return(operation); + } + + /// + /// We use these reusable operation wrappers to avoid capturing variables when serializing, to keep memory usage down while serializing. + /// + private sealed class SerializeFamilyOperation + { + // The first family member we serialize requires different serialization from the others. + public bool IsFirst; + public IMetricsSerializer? Serializer; + + public SerializeFamilyOperation() => Reset(); + + public void Reset() + { + IsFirst = true; + Serializer = null; + } + } + + // We have a bunch of families that get serialized often - no reason to churn the GC with a bunch of allocations if we can easily reuse it. + private static readonly ObjectPool _serializeFamilyOperationPool = ObjectPool.Create(new SerializeFamilyOperationPoolingPolicy()); + + private sealed class SerializeFamilyOperationPoolingPolicy : PooledObjectPolicy + { + public override SerializeFamilyOperation Create() => new(); + + public override bool Return(SerializeFamilyOperation obj) + { + obj.Reset(); + return true; + } + } + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private async ValueTask CollectAndSerialize(Collector collector, SerializeFamilyOperation operation, CancellationToken cancel) + { + await collector.CollectAndSerializeAsync(operation.Serializer!, operation.IsFirst, cancel); + operation.IsFirst = false; + } + + private readonly Func _collectAndSerializeFunc; + + internal Collector GetOrAdd( + in CollectorIdentity identity, + string name, + string help, + TConfiguration configuration, + ExemplarBehavior exemplarBehavior, + CollectorRegistry.CollectorInitializer initializer) + where TCollector : Collector + where TConfiguration : MetricConfiguration + { + // First we try just holding a read lock. This is the happy path. + _lock.EnterReadLock(); + + try + { + if (_collectors.TryGetValue(identity, out var collector)) + return collector; + } + finally + { + _lock.ExitReadLock(); + } + + // Then we grab a write lock. This is the slow path. + var newCollector = initializer(name, help, identity.InstanceLabelNames, identity.StaticLabels, configuration, exemplarBehavior); + + _lock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_collectors.TryAdd(identity, newCollector)) + return newCollector; + + return _collectors[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_collectors.TryGetValue(identity, out var collector)) + return collector; + + _collectors.Add(identity, newCollector); + return newCollector; +#endif + } + finally + { + _lock.ExitWriteLock(); + } + } + + internal void ForEachCollector(Action action) + { + _lock.EnterReadLock(); + + try + { + foreach (var collector in _collectors.Values) + action(collector); + } + finally + { + _lock.ExitReadLock(); + } + } + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + internal async ValueTask ForEachCollectorAsync(Func func, TArg arg, CancellationToken cancel) + where TArg : class + { + // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). + // Therefore we operate on a defensive copy in a reused buffer. + Collector[] buffer; + + _lock.EnterReadLock(); + + var collectorCount = _collectors.Count; + buffer = ArrayPool.Shared.Rent(collectorCount); + + try + { + try + { + _collectors.Values.CopyTo(buffer, 0); + } + finally + { + _lock.ExitReadLock(); + } + + for (var i = 0; i < collectorCount; i++) + { + var collector = buffer[i]; + await func(collector, arg, cancel); + } + } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: true); + } + } +} diff --git a/Prometheus/CollectorIdentity.cs b/Prometheus/CollectorIdentity.cs new file mode 100644 index 00000000..c89729bf --- /dev/null +++ b/Prometheus/CollectorIdentity.cs @@ -0,0 +1,59 @@ +namespace Prometheus; + +/// +/// Uniquely identifies a specific collector within a family. Different collectors are used for different label combinations. +/// * Any difference in static labels (keys or values) means it is a different collector. +/// * Any difference in the names of instance labels means it is a different collector. +/// +internal readonly struct CollectorIdentity(StringSequence instanceLabelNames, LabelSequence staticLabels) : IEquatable +{ + public readonly StringSequence InstanceLabelNames = instanceLabelNames; + public readonly LabelSequence StaticLabels = staticLabels; + + private readonly int _hashCode = CalculateHashCode(instanceLabelNames, staticLabels); + + public bool Equals(CollectorIdentity other) + { + if (_hashCode != other._hashCode) + return false; + + if (InstanceLabelNames.Length != other.InstanceLabelNames.Length) + return false; + + if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) + return false; + + if (!StaticLabels.Equals(other.StaticLabels)) + return false; + + return true; + } + + public override int GetHashCode() + { + return _hashCode; + } + + private static int CalculateHashCode(StringSequence instanceLabelNames, LabelSequence staticLabels) + { + unchecked + { + int hashCode = 0; + + hashCode ^= instanceLabelNames.GetHashCode() * 397; + hashCode ^= staticLabels.GetHashCode() * 397; + + return hashCode; + } + } + + public override string ToString() + { + return $"{_hashCode}{{{InstanceLabelNames.Length} + {StaticLabels.Length}}}"; + } + + public override bool Equals(object? obj) + { + return obj is CollectorIdentity identity && Equals(identity); + } +} \ No newline at end of file diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs new file mode 100644 index 00000000..35f1971c --- /dev/null +++ b/Prometheus/CollectorRegistry.cs @@ -0,0 +1,449 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace Prometheus; + +/// +/// Maintains references to a set of collectors, from which data for metrics is collected at data export time. +/// +/// Use methods on the class to add metrics to a collector registry. +/// +/// +/// To encourage good concurrency practices, registries are append-only. You can add things to them but not remove. +/// If you wish to remove things from the registry, create a new registry with only the things you wish to keep. +/// +public sealed class CollectorRegistry : ICollectorRegistry +{ + #region "Before collect" callbacks + /// + /// Registers an action to be called before metrics are collected. + /// This enables you to do last-minute updates to metric values very near the time of collection. + /// Callbacks will delay the metric collection, so do not make them too long or it may time out. + /// + /// The callback will be executed synchronously and should not take more than a few milliseconds. + /// To execute longer-duration callbacks, register an asynchronous callback (Func<Task>). + /// + /// If the callback throws then the entire metric collection will fail. + /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. + /// + /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. + /// + public void AddBeforeCollectCallback(Action callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + _beforeCollectCallbacks.Add(callback); + } + + /// + /// Registers an action to be called before metrics are collected. + /// This enables you to do last-minute updates to metric values very near the time of collection. + /// Callbacks will delay the metric collection, so do not make them too long or it may time out. + /// + /// Asynchronous callbacks will be executed concurrently and may last longer than a few milliseconds. + /// + /// If the callback throws then the entire metric collection will fail. + /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. + /// + /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. + /// + public void AddBeforeCollectCallback(Func callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + _beforeCollectAsyncCallbacks.Add(callback); + } + + private readonly ConcurrentBag _beforeCollectCallbacks = []; + private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = []; + #endregion + + #region Static labels + /// + /// The set of static labels that are applied to all metrics in this registry. + /// Enumeration of the returned collection is thread-safe. + /// + public IEnumerable> StaticLabels => _staticLabels.ToDictionary(); + + /// + /// Defines the set of static labels to apply to all metrics in this registry. + /// The static labels can only be set once on startup, before adding or publishing any metrics. + /// + public void SetStaticLabels(IDictionary labels) + { + if (labels == null) + throw new ArgumentNullException(nameof(labels)); + + // Read lock is taken when creating metrics, so we know that no metrics can be created while we hold this lock. + _staticLabelsLock.EnterWriteLock(); + + try + { + if (_staticLabels.Length != 0) + throw new InvalidOperationException("Static labels have already been defined - you can only do it once per registry."); + + if (_families.Count != 0) + throw new InvalidOperationException("Metrics have already been added to the registry - cannot define static labels anymore."); + + // Keep the lock for the duration of this method to make sure no publishing happens while we are setting labels. + lock (_firstCollectLock) + { + if (_hasPerformedFirstCollect) + throw new InvalidOperationException("The metrics registry has already been published - cannot define static labels anymore."); + + foreach (var pair in labels) + { + if (pair.Key == null) + throw new ArgumentException("The name of a label cannot be null."); + + if (pair.Value == null) + throw new ArgumentException("The value of a label cannot be null."); + + Collector.ValidateLabelName(pair.Key); + } + + _staticLabels = LabelSequence.From(labels); + } + } + finally + { + _staticLabelsLock.ExitWriteLock(); + } + } + + private LabelSequence _staticLabels; + private readonly ReaderWriterLockSlim _staticLabelsLock = new(); + + internal LabelSequence GetStaticLabels() + { + _staticLabelsLock.EnterReadLock(); + + try + { + return _staticLabels; + } + finally + { + _staticLabelsLock.ExitReadLock(); + } + } + #endregion + + /// + /// Collects all metrics and exports them in text document format to the provided stream. + /// + /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. + /// + public Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default) + => CollectAndExportAsTextAsync(to, ExpositionFormat.PrometheusText, cancel); + + /// + /// Collects all metrics and exports them in text document format to the provided stream. + /// + /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. + /// + public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format, CancellationToken cancel = default) + { + if (to == null) + throw new ArgumentNullException(nameof(to)); + + return CollectAndSerializeAsync(new TextSerializer(to, format), cancel); + } + + internal delegate TCollector CollectorInitializer(string name, string help, in StringSequence instanceLabelNames, in LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior) + where TCollector : Collector + where TConfiguration : MetricConfiguration; + + /// + /// Adds a collector to the registry, returning an existing instance if one with a matching name was already registered. + /// + internal TCollector GetOrAdd(string name, string help, in StringSequence instanceLabelNames, in LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior, in CollectorInitializer initializer) + where TCollector : Collector + where TConfiguration : MetricConfiguration + { + var family = GetOrAddCollectorFamily(name); + + var collectorIdentity = new CollectorIdentity(instanceLabelNames, staticLabels); + + return (TCollector)family.GetOrAdd(collectorIdentity, name, help, configuration, exemplarBehavior, initializer); + } + + private CollectorFamily GetOrAddCollectorFamily(string finalName) + where TCollector : Collector + { + static CollectorFamily ValidateFamily(CollectorFamily candidate) + { + // We either created a new collector family or found one with a matching identity. + // We do some basic validation here to avoid silly API usage mistakes. + + if (candidate.CollectorType != typeof(TCollector)) + throw new InvalidOperationException("Collector of a different type with the same name is already registered."); + + return candidate; + } + + // First try to get the family with only a read lock, with the assumption that it might already exist and therefore we do not need an expensive write lock. + _familiesLock.EnterReadLock(); + + try + { + if (_families.TryGetValue(finalName, out var existing)) + return ValidateFamily(existing); + } + finally + { + _familiesLock.ExitReadLock(); + } + + // It does not exist. OK, just create it. + var newFamily = new CollectorFamily(typeof(TCollector)); + + _familiesLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_families.TryAdd(finalName, newFamily)) + return newFamily; + + return ValidateFamily(_families[finalName]); +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_families.TryGetValue(finalName, out var existing)) + return ValidateFamily(existing); + + _families.Add(finalName, newFamily); + return newFamily; +#endif + } + finally + { + _familiesLock.ExitWriteLock(); + } + } + + // Each collector family has an identity (the base name of the metric, in Prometheus format) and any number of collectors within. + // Different collectors in the same family may have different sets of labels (static and instance) depending on how they were created. + private readonly Dictionary _families = new(StringComparer.Ordinal); + private readonly ReaderWriterLockSlim _familiesLock = new(); + + internal void SetBeforeFirstCollectCallback(Action a) + { + lock (_firstCollectLock) + { + if (_hasPerformedFirstCollect) + return; // Avoid keeping a reference to a callback we won't ever use. + + _beforeFirstCollectCallback = a; + } + } + + /// + /// Allows us to initialize (or not) the registry with the default metrics before the first collection. + /// + private Action? _beforeFirstCollectCallback; + private bool _hasPerformedFirstCollect; + private readonly object _firstCollectLock = new(); + + /// + /// Collects metrics from all the registered collectors and sends them to the specified serializer. + /// + internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + lock (_firstCollectLock) + { + if (!_hasPerformedFirstCollect) + { + _hasPerformedFirstCollect = true; + _beforeFirstCollectCallback?.Invoke(); + _beforeFirstCollectCallback = null; + } + } + + await RunBeforeCollectCallbacksAsync(cancel); + + UpdateRegistryMetrics(); + + // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). + // Therefore we operate on a defensive copy in a reused buffer. + CollectorFamily[] buffer; + + _familiesLock.EnterReadLock(); + + var familiesCount = _families.Count; + buffer = ArrayPool.Shared.Rent(familiesCount); + + try + { + try + { + _families.Values.CopyTo(buffer, 0); + } + finally + { + _familiesLock.ExitReadLock(); + } + + for (var i = 0; i < familiesCount; i++) + { + var family = buffer[i]; + await family.CollectAndSerializeAsync(serializer, cancel); + } + } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: true); + } + + await serializer.WriteEnd(cancel); + await serializer.FlushAsync(cancel); + } + + private async Task RunBeforeCollectCallbacksAsync(CancellationToken cancel) + { + foreach (var callback in _beforeCollectCallbacks) + { + try + { + callback(); + } + catch (Exception ex) + { + Trace.WriteLine($"Metrics before-collect callback failed: {ex}"); + } + } + + await Task.WhenAll(_beforeCollectAsyncCallbacks.Select(async (callback) => + { + try + { + await callback(cancel); + } + catch (Exception ex) + { + Trace.WriteLine($"Metrics before-collect callback failed: {ex}"); + } + })); + } + + /// + /// We collect some debug metrics from the registry itself to help indicate how many metrics we are publishing. + /// + internal void StartCollectingRegistryMetrics() + { + var factory = Metrics.WithCustomRegistry(this); + + _metricFamilies = factory.CreateGauge("prometheus_net_metric_families", "Number of metric families currently registered.", labelNames: [MetricTypeDebugLabel]); + _metricInstances = factory.CreateGauge("prometheus_net_metric_instances", "Number of metric instances currently registered across all metric families.", labelNames: [MetricTypeDebugLabel]); + _metricTimeseries = factory.CreateGauge("prometheus_net_metric_timeseries", "Number of metric timeseries currently generated from all metric instances.", labelNames: [MetricTypeDebugLabel]); + + _metricFamiliesPerType = []; + _metricInstancesPerType = []; + _metricTimeseriesPerType = []; + + foreach (MetricType type in Enum.GetValues(typeof(MetricType))) + { + var typeName = type.ToString().ToLowerInvariant(); + _metricFamiliesPerType[type] = _metricFamilies.WithLabels(typeName); + _metricInstancesPerType[type] = _metricInstances.WithLabels(typeName); + _metricTimeseriesPerType[type] = _metricTimeseries.WithLabels(typeName); + } + + _startedCollectingRegistryMetrics.SetResult(true); + } + + /// + /// Registers a callback to be called when registry debug metrics are enabled. + /// If the debug metrics have already been enabled, the callback is called immediately. + /// + internal void OnStartCollectingRegistryMetrics(Action callback) + { + _startedCollectingRegistryMetrics.Task.ContinueWith(delegate + { + callback(); + return Task.CompletedTask; + }); + } + + private readonly TaskCompletionSource _startedCollectingRegistryMetrics = new(); + + private const string MetricTypeDebugLabel = "metric_type"; + + private Gauge? _metricFamilies; + private Gauge? _metricInstances; + private Gauge? _metricTimeseries; + + private Dictionary? _metricFamiliesPerType; + private Dictionary? _metricInstancesPerType; + private Dictionary? _metricTimeseriesPerType; + + private void UpdateRegistryMetrics() + { + if (_metricFamiliesPerType == null || _metricInstancesPerType == null || _metricTimeseriesPerType == null) + return; // Debug metrics are not enabled. + + // We copy references to the metric families to a temporary buffer to avoid having to hold locks to keep the collection consistent. + CollectorFamily[] familiesBuffer; + + _familiesLock.EnterReadLock(); + + var familiesCount = _families.Count; + familiesBuffer = ArrayPool.Shared.Rent(familiesCount); + + try + { + try + { + _families.Values.CopyTo(familiesBuffer, 0); + } + finally + { + _familiesLock.ExitReadLock(); + } + + foreach (MetricType type in Enum.GetValues(typeof(MetricType))) + { + long families = 0; + long instances = 0; + long timeseries = 0; + + for (var i = 0; i < familiesCount; i++) + { + var family = familiesBuffer[i]; + + bool hadMatchingType = false; + + family.ForEachCollector(collector => + { + if (collector.Type != type) + return; + + hadMatchingType = true; + instances += collector.ChildCount; + timeseries += collector.TimeseriesCount; + }); + + if (hadMatchingType) + families++; + } + + _metricFamiliesPerType[type].Set(families); + _metricInstancesPerType[type].Set(instances); + _metricTimeseriesPerType[type].Set(timeseries); + } + } + finally + { + ArrayPool.Shared.Return(familiesBuffer, clearArray: true); + } + } + + // We only allow integration adapters to be started once per registry with the default configuration, to prevent double-counting values. + // This is useful because we switched on adapters by default in 7.0.0 but if someone has manual .StartListening() calls from before, they would now count metrics double. + internal bool PreventMeterAdapterRegistrationWithDefaultOptions; + internal bool PreventEventCounterAdapterRegistrationWithDefaultOptions; +} diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs new file mode 100644 index 00000000..bbcfa3ca --- /dev/null +++ b/Prometheus/Counter.cs @@ -0,0 +1,94 @@ +using System.Runtime.CompilerServices; + +namespace Prometheus; + +public sealed class Counter : Collector, ICounter +{ + public sealed class Child : ChildBase, ICounter + { + internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) + { + } + + private ThreadSafeDouble _value; + private ObservedExemplar _observedExemplar = ObservedExemplar.Empty; + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + var exemplar = BorrowExemplar(ref _observedExemplar); + + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + Value, + exemplar, + null, + cancel); + + ReturnBorrowedExemplar(ref _observedExemplar, exemplar); + } + + public void Inc(double increment = 1.0) + { + Inc(increment: increment, null); + } + + public void Inc(Exemplar? exemplar) + { + Inc(increment: 1, exemplar: exemplar); + } + + public void Inc(double increment, Exemplar? exemplar) + { + if (increment < 0.0) + throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); + + exemplar ??= GetDefaultExemplar(increment); + + if (exemplar?.Length > 0) + RecordExemplar(exemplar, ref _observedExemplar, increment); + + _value.Add(increment); + + Publish(); + } + + public void IncTo(double targetValue) + { + _value.IncrementTo(targetValue); + Publish(); + } + + public double Value => _value.Value; + } + + + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + } + + internal Counter(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) + { + } + + public void Inc(double increment = 1.0) => Unlabelled.Inc(increment); + public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); + public double Value => Unlabelled.Value; + + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + public void Inc(Exemplar? exemplar) => Inc(increment: 1, exemplar: exemplar); + public void Inc(double increment, Exemplar? exemplar) => Unlabelled.Inc(increment, exemplar); + + internal override MetricType Type => MetricType.Counter; + + internal override int TimeseriesCount => ChildCount; +} \ No newline at end of file diff --git a/Prometheus/CounterConfiguration.cs b/Prometheus/CounterConfiguration.cs new file mode 100644 index 00000000..42f713ca --- /dev/null +++ b/Prometheus/CounterConfiguration.cs @@ -0,0 +1,12 @@ +namespace Prometheus; + +public sealed class CounterConfiguration : MetricConfiguration +{ + internal static readonly CounterConfiguration Default = new(); + + /// + /// Allows you to configure how exemplars are applied to the published metric. + /// If null, inherits the exemplar behavior from the metric factory. + /// + public ExemplarBehavior? ExemplarBehavior { get; set; } +} diff --git a/Prometheus/CounterExtensions.cs b/Prometheus/CounterExtensions.cs new file mode 100644 index 00000000..9f5bbafa --- /dev/null +++ b/Prometheus/CounterExtensions.cs @@ -0,0 +1,116 @@ +namespace Prometheus; + +public static class CounterExtensions +{ + /// + /// Increments the value of the counter to the current UTC time as a Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToCurrentTimeUtc(this ICounter counter) + { + counter.IncTo(LowGranularityTimeSource.GetSecondsFromUnixEpoch()); + } + + /// + /// Increments the value of the counter to a specific moment as the UTC Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToTimeUtc(this ICounter counter, DateTimeOffset timestamp) + { + counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + } + + /// + /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static void CountExceptions(this ICounter counter, Action wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); + + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); + + try + { + wrapped(); + } + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) + { + counter.Inc(); + throw; + } + } + + /// + /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static TResult CountExceptions(this ICounter counter, Func wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); + + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); + + try + { + return wrapped(); + } + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) + { + counter.Inc(); + throw; + } + } + + /// + /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static async Task CountExceptionsAsync(this ICounter counter, Func wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); + + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); + + try + { + await wrapped().ConfigureAwait(false); + } + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) + { + counter.Inc(); + throw; + } + } + + /// + /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static async Task CountExceptionsAsync(this ICounter counter, Func> wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); + + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); + + try + { + return await wrapped().ConfigureAwait(false); + } + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) + { + counter.Inc(); + throw; + } + } +} diff --git a/Prometheus/DelegatingStreamInternal.cs b/Prometheus/DelegatingStreamInternal.cs new file mode 100644 index 00000000..1b8a6121 --- /dev/null +++ b/Prometheus/DelegatingStreamInternal.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// Modified for prometheus-net to reduce dependencies. + +#nullable enable + +namespace Prometheus; + +// Forwards all calls to an inner stream except where overridden in a derived class. +internal abstract class DelegatingStreamInternal : Stream +{ + private readonly Stream _innerStream; + + #region Properties + + public override bool CanRead + { + get { return _innerStream.CanRead; } + } + + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + public override bool CanWrite + { + get { return _innerStream.CanWrite; } + } + + public override long Length + { + get { return _innerStream.Length; } + } + + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } + + public override int ReadTimeout + { + get { return _innerStream.ReadTimeout; } + set { _innerStream.ReadTimeout = value; } + } + + public override bool CanTimeout + { + get { return _innerStream.CanTimeout; } + } + + public override int WriteTimeout + { + get { return _innerStream.WriteTimeout; } + set { _innerStream.WriteTimeout = value; } + } + + #endregion Properties + + protected DelegatingStreamInternal(Stream innerStream) + { + _innerStream = innerStream; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerStream.Dispose(); + } + base.Dispose(disposing); + } + + + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _innerStream.Read(buffer, offset, count); + } + + public override int ReadByte() + { + return _innerStream.ReadByte(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + } + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return _innerStream.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _innerStream.EndRead(asyncResult); + } + + public override void Flush() + { + _innerStream.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _innerStream.FlushAsync(cancellationToken); + } + + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _innerStream.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + _innerStream.WriteByte(value); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return _innerStream.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _innerStream.EndWrite(asyncResult); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); + } +} diff --git a/Prometheus/DiagnosticSourceAdapter.cs b/Prometheus/DiagnosticSourceAdapter.cs new file mode 100644 index 00000000..483482e4 --- /dev/null +++ b/Prometheus/DiagnosticSourceAdapter.cs @@ -0,0 +1,118 @@ +#if NET +using System.Diagnostics; + +namespace Prometheus; + +/// +/// Monitors all DiagnosticSource events and exposes them as Prometheus counters. +/// The event data is discarded, only the number of occurrences is measured. +/// +/// +/// This is a very coarse data set due to lacking any intelligence on the payload. +/// Users are recommended to make custom adapters with more detail for specific use cases. +/// +public sealed class DiagnosticSourceAdapter : IDisposable +{ + /// + /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. + /// Dispose of the return value to stop listening. + /// + public static IDisposable StartListening() => StartListening(DiagnosticSourceAdapterOptions.Default); + + /// + /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. + /// Dispose of the return value to stop listening. + /// + public static IDisposable StartListening(DiagnosticSourceAdapterOptions options) => new DiagnosticSourceAdapter(options); + + private DiagnosticSourceAdapter(DiagnosticSourceAdapterOptions options) + { + _options = options; + _metric = Metrics.WithCustomRegistry(options.Registry) + .CreateCounter("diagnostic_events_total", "Total count of events received via the DiagnosticSource infrastructure.", labelNames: new[] + { + "source", // Name of the DiagnosticSource + "event" // Name of the event + }); + + var newListenerObserver = new NewListenerObserver(OnNewListener); + _newListenerSubscription = DiagnosticListener.AllListeners.Subscribe(newListenerObserver); + } + + private readonly DiagnosticSourceAdapterOptions _options; + private readonly Counter _metric; + + private readonly IDisposable _newListenerSubscription; + + // listener name -> subscription + private readonly Dictionary _newEventSubscription = new Dictionary(); + private readonly object _newEventSubscriptionLock = new object(); + + private void OnNewListener(DiagnosticListener listener) + { + lock (_newEventSubscriptionLock) + { + if (_newEventSubscription.TryGetValue(listener.Name, out var oldSubscription)) + { + oldSubscription.Dispose(); + _newEventSubscription.Remove(listener.Name); + } + + if (!_options.ListenerFilterPredicate(listener)) + return; + + var listenerName = listener.Name; + var newEventObserver = new NewEventObserver(kvp => OnEvent(listenerName, kvp.Key, kvp.Value)); + _newEventSubscription[listenerName] = listener.Subscribe(newEventObserver); + } + } + + private void OnEvent(string listenerName, string eventName, object? payload) + { + _metric.WithLabels(listenerName, eventName).Inc(); + } + + private sealed class NewListenerObserver(Action onNewListener) : IObserver + { + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(DiagnosticListener listener) + { + onNewListener(listener); + } + } + + private sealed class NewEventObserver(Action> onEvent) : IObserver> + { + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(KeyValuePair receivedEvent) + { + onEvent(receivedEvent); + } + } + + public void Dispose() + { + _newListenerSubscription.Dispose(); + + lock (_newEventSubscriptionLock) + { + foreach (var subscription in _newEventSubscription.Values) + subscription.Dispose(); + } + } +} +#endif \ No newline at end of file diff --git a/Prometheus/DiagnosticSourceAdapterOptions.cs b/Prometheus/DiagnosticSourceAdapterOptions.cs new file mode 100644 index 00000000..b86ecda8 --- /dev/null +++ b/Prometheus/DiagnosticSourceAdapterOptions.cs @@ -0,0 +1,17 @@ +#if NET +using System.Diagnostics; + +namespace Prometheus; + +public sealed class DiagnosticSourceAdapterOptions +{ + internal static readonly DiagnosticSourceAdapterOptions Default = new(); + + /// + /// By default we subscribe to all listeners but this allows you to filter by listener. + /// + public Func ListenerFilterPredicate = _ => true; + + public CollectorRegistry Registry = Metrics.DefaultRegistry; +} +#endif \ No newline at end of file diff --git a/Prometheus/DotNetStats.cs b/Prometheus/DotNetStats.cs new file mode 100644 index 00000000..1de75eab --- /dev/null +++ b/Prometheus/DotNetStats.cs @@ -0,0 +1,98 @@ +using System.Diagnostics; + +namespace Prometheus; + +/// +/// Collects basic .NET metrics about the current process. This is not meant to be an especially serious collector, +/// more of a producer of sample data so users of the library see something when they install it. +/// +public sealed class DotNetStats +{ + /// + /// Registers the .NET metrics in the specified registry. + /// + public static void Register(CollectorRegistry registry) + { + var instance = new DotNetStats(Metrics.WithCustomRegistry(registry)); + registry.AddBeforeCollectCallback(instance.UpdateMetrics); + } + + /// + /// Registers the .NET metrics in the default metrics factory and registry. + /// + internal static void RegisterDefault() + { + var instance = new DotNetStats(Metrics.DefaultFactory); + Metrics.DefaultRegistry.AddBeforeCollectCallback(instance.UpdateMetrics); + } + + private readonly Process _process; + private readonly List _collectionCounts = new List(); + private Gauge _totalMemory; + private Gauge _virtualMemorySize; + private Gauge _workingSet; + private Gauge _privateMemorySize; + private Counter _cpuTotal; + private Gauge _openHandles; + private Gauge _startTime; + private Gauge _numThreads; + + private DotNetStats(IMetricFactory metricFactory) + { + _process = Process.GetCurrentProcess(); + + var collectionCountsParent = metricFactory.CreateCounter("dotnet_collection_count_total", "GC collection count", new[] { "generation" }); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + _collectionCounts.Add(collectionCountsParent.Labels(gen.ToString())); + } + + // Metrics that make sense to compare between all operating systems + // Note that old versions of pushgateway errored out if different metrics had same name but different help string. + // This is fixed in newer versions but keep the help text synchronized with the Go implementation just in case. + // See https://github.com/prometheus/pushgateway/issues/194 + // and https://github.com/prometheus-net/prometheus-net/issues/89 + _startTime = metricFactory.CreateGauge("process_start_time_seconds", "Start time of the process since unix epoch in seconds."); + _cpuTotal = metricFactory.CreateCounter("process_cpu_seconds_total", "Total user and system CPU time spent in seconds."); + + _virtualMemorySize = metricFactory.CreateGauge("process_virtual_memory_bytes", "Virtual memory size in bytes."); + _workingSet = metricFactory.CreateGauge("process_working_set_bytes", "Process working set"); + _privateMemorySize = metricFactory.CreateGauge("process_private_memory_bytes", "Process private memory size"); + _openHandles = metricFactory.CreateGauge("process_open_handles", "Number of open handles"); + _numThreads = metricFactory.CreateGauge("process_num_threads", "Total number of threads"); + + // .net specific metrics + _totalMemory = metricFactory.CreateGauge("dotnet_total_memory_bytes", "Total known allocated memory"); + + _startTime.SetToTimeUtc(_process.StartTime); + } + + // The Process class is not thread-safe so let's synchronize the updates to avoid data tearing. + private readonly object _updateLock = new object(); + + private void UpdateMetrics() + { + try + { + lock (_updateLock) + { + _process.Refresh(); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + _collectionCounts[gen].IncTo(GC.CollectionCount(gen)); + + _totalMemory.Set(GC.GetTotalMemory(false)); + _virtualMemorySize.Set(_process.VirtualMemorySize64); + _workingSet.Set(_process.WorkingSet64); + _privateMemorySize.Set(_process.PrivateMemorySize64); + _cpuTotal.IncTo(_process.TotalProcessorTime.TotalSeconds); + _openHandles.Set(_process.HandleCount); + _numThreads.Set(_process.Threads.Count); + } + } + catch (Exception) + { + } + } +} \ No newline at end of file diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs new file mode 100644 index 00000000..78113998 --- /dev/null +++ b/Prometheus/EventCounterAdapter.cs @@ -0,0 +1,238 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Globalization; + +namespace Prometheus; + +/// +/// Monitors .NET EventCounters and exposes them as Prometheus metrics. +/// +/// +/// All observed .NET event counters are transformed into Prometheus metrics with translated names. +/// +public sealed class EventCounterAdapter : IDisposable +{ + public static IDisposable StartListening() => StartListening(EventCounterAdapterOptions.Default); + + public static IDisposable StartListening(EventCounterAdapterOptions options) + { + // If we are re-registering an adapter with the default options, just pretend and move on. + // The purpose of this code is to avoid double-counting metrics if the adapter is registered twice with the default options. + // This could happen because in 7.0.0 we added automatic registration of the adapters on startup, but the user might still + // have a manual registration active from 6.0.0 days. We do this small thing here to make upgrading less hassle. + if (options == EventCounterAdapterOptions.Default) + { + if (options.Registry.PreventEventCounterAdapterRegistrationWithDefaultOptions) + return new NoopDisposable(); + + options.Registry.PreventEventCounterAdapterRegistrationWithDefaultOptions = true; + } + + return new EventCounterAdapter(options); + } + + private EventCounterAdapter(EventCounterAdapterOptions options) + { + _options = options; + _metricFactory = _options.MetricFactory ?? Metrics.WithCustomRegistry(_options.Registry); + + _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); + + EventCounterAdapterMemoryWarden.EnsureStarted(); + + _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, options.UpdateInterval, OnEventWritten); + } + + public void Dispose() + { + // Disposal means we stop listening but we do not remove any published data just to keep things simple. + _listener.Dispose(); + } + + private readonly EventCounterAdapterOptions _options; + private readonly IMetricFactory _metricFactory; + + private readonly Listener _listener; + + // We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a gauge. + private readonly Gauge _eventSourcesConnected; + + private bool ShouldUseEventSource(EventSource source) + { + bool connect = _options.EventSourceFilterPredicate(source.Name); + + if (connect) + _eventSourcesConnected.Inc(); + + return connect; + } + + private EventCounterAdapterEventSourceSettings ConfigureEventSource(EventSource source) + { + return _options.EventSourceSettingsProvider(source.Name); + } + + private const string RateSuffix = "_rate"; + + private void OnEventWritten(EventWrittenEventArgs args) + { + // This deserialization here is pretty gnarly. + // We just skip anything that makes no sense. + + try + { + if (args.EventName != "EventCounters") + return; // Do not know what it is and do not care. + + if (args.Payload == null) + return; // What? Whatever. + + var eventSourceName = args.EventSource.Name; + + foreach (var item in args.Payload) + { + if (item is not IDictionary e) + continue; + + if (!e.TryGetValue("Name", out var nameWrapper)) + continue; + + var name = nameWrapper as string; + + if (name == null) + continue; // What? Whatever. + + if (!e.TryGetValue("DisplayName", out var displayNameWrapper)) + continue; + + var displayName = displayNameWrapper as string ?? ""; + + // If there is a DisplayUnits, prefix it to the help text. + if (e.TryGetValue("DisplayUnits", out var displayUnitsWrapper) && !string.IsNullOrWhiteSpace(displayUnitsWrapper as string)) + displayName = $"({(string)displayUnitsWrapper}) {displayName}"; + + var mergedName = $"{eventSourceName}_{name}"; + + var prometheusName = _counterPrometheusName.GetOrAdd(mergedName, PrometheusNameHelpers.TranslateNameToPrometheusName); + + // The event counter can either be + // 1) an aggregating counter (in which case we use the mean); or + // 2) an incrementing counter (in which case we use the delta). + + if (e.TryGetValue("Increment", out var increment)) + { + // Looks like an incrementing counter. + + var value = increment as double?; + + if (value == null) + continue; // What? Whatever. + + // If the underlying metric is exposing a rate then this can result in some strange terminology like "rate_total". + // We will remove the "rate" from the name to be more understandable - you'll get the rate when you apply the Prometheus rate() function, the raw value is not the rate. + if (prometheusName.EndsWith(RateSuffix)) + prometheusName = prometheusName.Remove(prometheusName.Length - RateSuffix.Length); + + _metricFactory.CreateCounter(prometheusName + "_total", displayName).Inc(value.Value); + } + else if (e.TryGetValue("Mean", out var mean)) + { + // Looks like an aggregating counter. + + var value = mean as double?; + + if (value == null) + continue; // What? Whatever. + + _metricFactory.CreateGauge(prometheusName, displayName).Set(value.Value); + } + } + } + catch (Exception ex) + { + // We do not want to throw any exceptions if we fail to handle this event because who knows what it messes up upstream. + Trace.WriteLine($"Failed to parse EventCounter event: {ex.Message}"); + } + } + + // Source+Name -> Name + private readonly ConcurrentDictionary _counterPrometheusName = new(); + + private sealed class Listener : EventListener + { + public Listener( + Func shouldUseEventSource, + Func configureEventSosurce, + TimeSpan updateInterval, + Action onEventWritten) + { + _shouldUseEventSource = shouldUseEventSource; + _configureEventSosurce = configureEventSosurce; + _updateInterval = updateInterval; + _onEventWritten = onEventWritten; + + foreach (var eventSource in _preRegisteredEventSources) + OnEventSourceCreated(eventSource); + + _preRegisteredEventSources.Clear(); + } + + private readonly List _preRegisteredEventSources = new List(); + + private readonly Func _shouldUseEventSource; + private readonly Func _configureEventSosurce; + private readonly TimeSpan _updateInterval; + private readonly Action _onEventWritten; + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (_shouldUseEventSource == null) + { + // The way this EventListener thing works is rather strange. Immediately in the base class constructor, before we + // have even had time to wire up our subclass, it starts calling OnEventSourceCreated for all already-existing event sources... + // We just buffer those calls because CALM DOWN SIR! + _preRegisteredEventSources.Add(eventSource); + return; + } + + if (!_shouldUseEventSource(eventSource)) + return; + + try + { + var options = _configureEventSosurce(eventSource); + + EnableEvents(eventSource, options.MinimumLevel, options.MatchKeywords, new Dictionary() + { + ["EventCounterIntervalSec"] = ((int)Math.Max(1, _updateInterval.TotalSeconds)).ToString(CultureInfo.InvariantCulture), + }); + } + catch (Exception ex) + { + // Eat exceptions here to ensure no harm comes of failed enabling. + // The EventCounter infrastructure has proven quite buggy and while it is not certain that this may throw, let's be paranoid. + Trace.WriteLine($"Failed to enable EventCounter listening for {eventSource.Name}: {ex.Message}"); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + _onEventWritten(eventData); + } + } + + /// + /// By default we enable event sources that start with any of these strings. This is a manually curated list to try enable some useful ones + /// without just enabling everything under the sky (because .NET has no way to say "enable only the event counters", you have to enable all diagnostic events). + /// + private static readonly IReadOnlyList DefaultEventSourcePrefixes = new[] + { + "System.Runtime", + "Microsoft-AspNetCore", + "Microsoft.AspNetCore", + "System.Net" + }; + + public static readonly Func DefaultEventSourceFilterPredicate = name => DefaultEventSourcePrefixes.Any(x => name.StartsWith(x, StringComparison.Ordinal)); +} diff --git a/Prometheus/EventCounterAdapterEventSourceSettings.cs b/Prometheus/EventCounterAdapterEventSourceSettings.cs new file mode 100644 index 00000000..ab08dd2f --- /dev/null +++ b/Prometheus/EventCounterAdapterEventSourceSettings.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.Tracing; + +namespace Prometheus; + +/// +/// Defines how the EventCounterAdapter will subscribe to an event source. +/// +public sealed class EventCounterAdapterEventSourceSettings +{ + /// + /// Minimum level of events to receive. + /// + public EventLevel MinimumLevel { get; set; } = EventLevel.Informational; + + /// + /// Event keywords, of which at least one must match for an event to be received. + /// + public EventKeywords MatchKeywords { get; set; } = EventKeywords.None; +} diff --git a/Prometheus/EventCounterAdapterMemoryWarden.cs b/Prometheus/EventCounterAdapterMemoryWarden.cs new file mode 100644 index 00000000..a43abff3 --- /dev/null +++ b/Prometheus/EventCounterAdapterMemoryWarden.cs @@ -0,0 +1,41 @@ +namespace Prometheus; + +/// +/// .NET EventCounters are very noisy in terms of generating a lot of garbage. At the same time, apps in development environments typically do not get loaded much, so rarely collect garbage. +/// This can mean that as soon as you plug prometheus-net into an app, its memory usage shoots up due to gen 0 garbage piling up. It will all get collected... eventually, when the GC runs. +/// This might not happen for 12+ hours! It presents a major user perception issue, as they just see the process memory usage rise and rise and rise. +/// +/// This class exists to prevent this problem. We simply force a gen 0 GC every N minutes if EventCounterAdapter is enabled and if no GC has occurred in the last N minutes already. +/// +internal static class EventCounterAdapterMemoryWarden +{ + private static readonly TimeSpan ForcedCollectionInterval = TimeSpan.FromMinutes(10); + + public static void EnsureStarted() + { + // The constructor does all the work, this is just here to signal intent. + } + + static EventCounterAdapterMemoryWarden() + { + Task.Run(Execute); + } + + private static async Task Execute() + { + while (true) + { + // Capture pre-delay state so we can check if a collection is required. + var preDelayCollectionCount = GC.CollectionCount(0); + + await Task.Delay(ForcedCollectionInterval); + + var postDelayCollectionCount = GC.CollectionCount(0); + + if (preDelayCollectionCount != postDelayCollectionCount) + continue; // GC already happened, go chill. + + GC.Collect(0); + } + } +} diff --git a/Prometheus/EventCounterAdapterOptions.cs b/Prometheus/EventCounterAdapterOptions.cs new file mode 100644 index 00000000..55db8200 --- /dev/null +++ b/Prometheus/EventCounterAdapterOptions.cs @@ -0,0 +1,33 @@ +namespace Prometheus; + +public sealed record EventCounterAdapterOptions +{ + public static EventCounterAdapterOptions Default => new(); + + /// + /// By default we subscribe to a predefined set of generally useful event counters but this allows you to specify a custom filter by event source name. + /// + public Func EventSourceFilterPredicate { get; set; } = EventCounterAdapter.DefaultEventSourceFilterPredicate; + + /// + /// By default, we subscribe to event counters at Informational level from every event source. + /// You can customize these settings via this callback (with the event source name as the string given as input). + /// + public Func EventSourceSettingsProvider { get; set; } = _ => new(); + + /// + /// How often we update event counter data. + /// + /// + /// Event counters are quite noisy in terms of generating a lot of temporary objects in memory, so we keep the default moderate. + /// All this memory is immediately GC-able but in a near-idle app it can make for a scary upward trend on the RAM usage graph because the GC might not immediately release the memory to the OS. + /// + public TimeSpan UpdateInterval { get; set; } = TimeSpan.FromSeconds(10); + + public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; + + /// + /// If set, the value in Registry is ignored and this factory is instead used to create all the metrics. + /// + public IMetricFactory? MetricFactory { get; set; } = Metrics.DefaultFactory; +} diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs new file mode 100644 index 00000000..c9726d30 --- /dev/null +++ b/Prometheus/Exemplar.cs @@ -0,0 +1,304 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.ObjectPool; + +namespace Prometheus; + +/// +/// A fully-formed exemplar, describing a set of label name-value pairs. +/// +/// One-time use only - when you pass an instance to a prometheus-net method, it will take ownership of it. +/// +/// You should preallocate and cache: +/// 1. The exemplar keys created via Exemplar.Key(). +/// 2. Exemplar key-value pairs created vvia key.WithValue() or Exemplar.Pair(). +/// +/// From the key-value pairs you can create one-use Exemplar values using Exemplar.From(). +/// You can clone Exemplar instances using Exemplar.Clone() - each clone can only be used once! +/// +public sealed class Exemplar +{ + /// + /// Indicates that no exemplar is to be recorded for a given observation. + /// + public static readonly Exemplar None = new(0); + + /// + /// An exemplar label key. For optimal performance, create it once and reuse it forever. + /// + public readonly struct LabelKey + { + internal LabelKey(byte[] key) + { + Bytes = key; + } + + // We only support ASCII here, so rune count always matches byte count. + internal int RuneCount => Bytes.Length; + + internal byte[] Bytes { get; } + + /// + /// Create a LabelPair once a value is available + /// + /// The string is expected to only contain runes in the ASCII range, runes outside the ASCII range will get replaced + /// with placeholders. This constraint may be relaxed with future versions. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LabelPair WithValue(string value) + { + static bool IsAscii(ReadOnlySpan chars) + { + for (var i = 0; i < chars.Length; i++) + if (chars[i] > 127) + return false; + + return true; + } + + if (!IsAscii(value.AsSpan())) + { + // We believe that approximately 100% of use cases only consist of ASCII characters. + // That being said, we do not want to throw an exception here as the value may be coming from external sources + // that calling code has little control over. Therefore, we just replace such characters with placeholders. + // This matches the default behavior of Encoding.ASCII.GetBytes() - it replaces non-ASCII characters with '?'. + // As this is a highly theoretical case, we do an inefficient conversion here using the built-in encoder. + value = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(value)); + } + + return new LabelPair(Bytes, value); + } + } + + /// + /// A single exemplar label pair in a form suitable for efficient serialization. + /// If you wish to reuse the same key-value pair, you should reuse this object as much as possible. + /// + public readonly struct LabelPair + { + internal LabelPair(byte[] keyBytes, string value) + { + KeyBytes = keyBytes; + Value = value; + } + + internal int RuneCount => KeyBytes.Length + Value.Length; + internal byte[] KeyBytes { get; } + + // We keep the value as a string because it typically starts out its life as a string + // and we want to avoid paying the cost of converting it to a byte array until we serialize it. + // If we record many exemplars then we may, in fact, never serialize most of them because they get replaced. + internal string Value { get; } + } + + /// + /// Return an exemplar label key, this may be curried with a value to produce a LabelPair. + /// Reuse this for optimal performance. + /// + public static LabelKey Key(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("empty key", nameof(key)); + + Collector.ValidateLabelName(key); + + var asciiBytes = Encoding.ASCII.GetBytes(key); + return new LabelKey(asciiBytes); + } + + /// + /// Pair constructs a LabelPair, it is advisable to memoize a "Key" (eg: "traceID") and then to derive "LabelPair"s + /// from these. You may (should) reuse a LabelPair for recording multiple observations that use the same exemplar. + /// + public static LabelPair Pair(string key, string value) + { + return Key(key).WithValue(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5, in LabelPair labelPair6) + { + var exemplar = Exemplar.AllocateFromPool(length: 6); + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + exemplar.LabelPair4 = labelPair4; + exemplar.LabelPair5 = labelPair5; + exemplar.LabelPair6 = labelPair6; + + return exemplar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5) + { + var exemplar = Exemplar.AllocateFromPool(length: 5); + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + exemplar.LabelPair4 = labelPair4; + exemplar.LabelPair5 = labelPair5; + + return exemplar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4) + { + var exemplar = Exemplar.AllocateFromPool(length: 4); + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + exemplar.LabelPair4 = labelPair4; + + return exemplar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3) + { + var exemplar = Exemplar.AllocateFromPool(length: 3); + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + + return exemplar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2) + { + var exemplar = Exemplar.AllocateFromPool(length: 2); + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + + return exemplar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exemplar From(in LabelPair labelPair1) + { + var exemplar = Exemplar.AllocateFromPool(length: 1); + exemplar.LabelPair1 = labelPair1; + + return exemplar; + } + + internal ref LabelPair this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (index == 0) return ref LabelPair1; + if (index == 1) return ref LabelPair2; + if (index == 2) return ref LabelPair3; + if (index == 3) return ref LabelPair4; + if (index == 4) return ref LabelPair5; + if (index == 5) return ref LabelPair6; + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ + private static readonly LabelKey DefaultTraceIdKey = Key("trace_id"); + private static readonly LabelKey DefaultSpanIdKey = Key("span_id"); + + public static Exemplar FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); + + public static Exemplar FromTraceContext(in LabelKey traceIdKey, in LabelKey spanIdKey) + { +#if NET6_0_OR_GREATER + var activity = Activity.Current; + if (activity != null) + { + // These values already exist as strings inside the Activity logic, so there is no string allocation happening here. + var traceIdLabel = traceIdKey.WithValue(activity.TraceId.ToString()); + var spanIdLabel = spanIdKey.WithValue(activity.SpanId.ToString()); + + return From(traceIdLabel, spanIdLabel); + } +#endif + + // Trace context based exemplars are only supported in .NET Core, not .NET Framework. + return None; + } + + public Exemplar() + { + } + + private Exemplar(int length) + { + Length = length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Update(int length) + { + Length = length; + Interlocked.Exchange(ref _consumed, IsNotConsumed); + } + + /// + /// Number of label pairs in use. + /// + internal int Length { get; private set; } + + internal LabelPair LabelPair1; + internal LabelPair LabelPair2; + internal LabelPair LabelPair3; + internal LabelPair LabelPair4; + internal LabelPair LabelPair5; + internal LabelPair LabelPair6; + + private static readonly ObjectPool ExemplarPool = ObjectPool.Create(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Exemplar AllocateFromPool(int length) + { + var instance = ExemplarPool.Get(); + instance.Update(length); + return instance; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ReturnToPoolIfNotEmpty() + { + if (Length == 0) + return; // Only the None instance can have a length of 0. + + Length = 0; + + ExemplarPool.Return(this); + } + + private long _consumed; + + private const long IsConsumed = 1; + private const long IsNotConsumed = 0; + + internal void MarkAsConsumed() + { + if (Interlocked.Exchange(ref _consumed, IsConsumed) == IsConsumed) + throw new InvalidOperationException($"An instance of {nameof(Exemplar)} was reused. You must obtain a new instance via Exemplar.From() or Exemplar.Clone() for each metric value observation."); + } + + /// + /// Clones the exemplar so it can be reused - each copy can only be used once! + /// + public Exemplar Clone() + { + if (Interlocked.Read(ref _consumed) == IsConsumed) + throw new InvalidOperationException($"An instance of {nameof(Exemplar)} cannot be cloned after it has already been used."); + + var clone = AllocateFromPool(Length); + clone.LabelPair1 = LabelPair1; + clone.LabelPair2 = LabelPair2; + clone.LabelPair3 = LabelPair3; + clone.LabelPair4 = LabelPair4; + clone.LabelPair5 = LabelPair5; + clone.LabelPair6 = LabelPair6; + return clone; + } +} \ No newline at end of file diff --git a/Prometheus/ExemplarBehavior.cs b/Prometheus/ExemplarBehavior.cs new file mode 100644 index 00000000..5cd584c6 --- /dev/null +++ b/Prometheus/ExemplarBehavior.cs @@ -0,0 +1,30 @@ +namespace Prometheus; + +/// +/// Defines how exemplars are obtained and published for metrics. +/// Different metrics can have their own exemplar behavior or simply inherit one from the metric factory. +/// +public sealed class ExemplarBehavior +{ + /// + /// Callback that provides the default exemplar if none is provided by the caller when providing a metric value. + /// Defaults to Exemplar.FromTraceContext(). + /// + public ExemplarProvider? DefaultExemplarProvider { get; set; } + + /// + /// A new exemplar will only be recorded for a timeseries if at least this much time has passed since the previous exemplar was recorded. + /// This can be used to limit the rate of publishing unique exemplars. By default we do not have any limit - a new exemplar always overwrites the old one. + /// + public TimeSpan NewExemplarMinInterval { get; set; } = TimeSpan.Zero; + + internal static readonly ExemplarBehavior Default = new() + { + DefaultExemplarProvider = (_, _) => Exemplar.FromTraceContext() + }; + + public static ExemplarBehavior NoExemplars() => new() + { + DefaultExemplarProvider = (_, _) => Exemplar.None + }; +} diff --git a/Prometheus/ExemplarProvider.cs b/Prometheus/ExemplarProvider.cs new file mode 100644 index 00000000..bd20af04 --- /dev/null +++ b/Prometheus/ExemplarProvider.cs @@ -0,0 +1,8 @@ +namespace Prometheus; + +/// +/// Callback to provide an exemplar for a specific observation. +/// +/// The metric instance for which an exemplar is being provided. +/// Context-dependent - for counters, the increment; for histograms, the observed value. +public delegate Exemplar ExemplarProvider(Collector metric, double value); diff --git a/Prometheus/ExpositionFormats.cs b/Prometheus/ExpositionFormats.cs new file mode 100644 index 00000000..02d811a5 --- /dev/null +++ b/Prometheus/ExpositionFormats.cs @@ -0,0 +1,13 @@ +namespace Prometheus; + +public enum ExpositionFormat +{ + /// + /// The traditional prometheus exposition format. + /// + PrometheusText, + /// + /// The OpenMetrics text exposition format + /// + OpenMetricsText +} \ No newline at end of file diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs new file mode 100644 index 00000000..c0ed8a12 --- /dev/null +++ b/Prometheus/Gauge.cs @@ -0,0 +1,74 @@ +namespace Prometheus; + +public sealed class Gauge : Collector, IGauge +{ + public sealed class Child : ChildBase, IGauge + { + internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) + { + } + + private ThreadSafeDouble _value; + + private protected override ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + return serializer.WriteMetricPointAsync( + Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, Value, ObservedExemplar.Empty, null, cancel); + } + + public void Inc(double increment = 1) + { + _value.Add(increment); + Publish(); + } + + public void Set(double val) + { + _value.Value = val; + Publish(); + } + + public void Dec(double decrement = 1) + { + Inc(-decrement); + } + + public void IncTo(double targetValue) + { + _value.IncrementTo(targetValue); + Publish(); + } + + public void DecTo(double targetValue) + { + _value.DecrementTo(targetValue); + Publish(); + } + + public double Value => _value.Value; + } + + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + } + + internal Gauge(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) + { + } + + public void Inc(double increment = 1) => Unlabelled.Inc(increment); + public void Set(double val) => Unlabelled.Set(val); + public void Dec(double decrement = 1) => Unlabelled.Dec(decrement); + public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); + public void DecTo(double targetValue) => Unlabelled.DecTo(targetValue); + public double Value => Unlabelled.Value; + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + internal override MetricType Type => MetricType.Gauge; + + internal override int TimeseriesCount => ChildCount; +} \ No newline at end of file diff --git a/Prometheus/GaugeConfiguration.cs b/Prometheus/GaugeConfiguration.cs new file mode 100644 index 00000000..fea1b1f1 --- /dev/null +++ b/Prometheus/GaugeConfiguration.cs @@ -0,0 +1,6 @@ +namespace Prometheus; + +public sealed class GaugeConfiguration : MetricConfiguration +{ + internal static readonly GaugeConfiguration Default = new(); +} diff --git a/Prometheus/GaugeExtensions.cs b/Prometheus/GaugeExtensions.cs new file mode 100644 index 00000000..0a6062b9 --- /dev/null +++ b/Prometheus/GaugeExtensions.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Prometheus; + +public static class GaugeExtensions +{ + /// + /// Sets the value of the gauge to the current UTC time as a Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// + public static void SetToCurrentTimeUtc(this IGauge gauge) + { + gauge.Set(LowGranularityTimeSource.GetSecondsFromUnixEpoch()); + } + + /// + /// Sets the value of the gauge to a specific moment as the UTC timezone Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// + public static void SetToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) + { + gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + } + + /// + /// Increments the value of the gauge to the current UTC time as a Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToCurrentTimeUtc(this IGauge gauge) + { + gauge.IncTo(LowGranularityTimeSource.GetSecondsFromUnixEpoch()); + } + + /// + /// Increments the value of the gauge to a specific moment as the UTC Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) + { + gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + } + + private sealed class InProgressTracker : IDisposable + { + public void Dispose() + { + if (_gauge == null) + return; + + _gauge.Dec(); + _gauge = null; + Pool.Return(this); + } + + private IGauge? _gauge; + + public void Update(IGauge gauge) + { + if (_gauge != null) + throw new InvalidOperationException($"{nameof(InProgressTracker)} was reused before being disposed."); + + _gauge = gauge; + } + + public static InProgressTracker Create(IGauge gauge) + { + var instance = Pool.Get(); + instance.Update(gauge); + return instance; + } + + private static readonly ObjectPool Pool = ObjectPool.Create(); + } + + /// + /// Tracks the number of in-progress operations taking place. + /// + /// Calling this increments the gauge. Disposing of the returned instance decrements it again. + /// + /// + /// It is safe to track the sum of multiple concurrent in-progress operations with the same gauge. + /// + public static IDisposable TrackInProgress(this IGauge gauge) + { + if (gauge == null) + throw new ArgumentNullException(nameof(gauge)); + + gauge.Inc(); + + return InProgressTracker.Create(gauge); + } +} diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs new file mode 100644 index 00000000..c2755c2e --- /dev/null +++ b/Prometheus/Histogram.cs @@ -0,0 +1,403 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +#if NET7_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace Prometheus; + +/// +/// The histogram is thread-safe but not atomic - the sum of values and total count of events +/// may not add up perfectly with bucket contents if new observations are made during a collection. +/// +public sealed class Histogram : Collector, IHistogram +{ + private static readonly double[] DefaultBuckets = [.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10]; + + private readonly double[] _buckets; + +#if NET7_0_OR_GREATER + // For AVX, we need to align on 32 bytes and pin the memory. This is a buffer + // with extra items that we can "skip" when using the data, for alignment purposes. + private readonly double[] _bucketsAlignmentBuffer; + // How many items from the start to skip. + private readonly int _bucketsAlignmentBufferOffset; + + private const int AvxAlignBytes = 32; +#endif + + // These labels go together with the buckets, so we do not need to allocate them for every child. + private readonly CanonicalLabel[] _leLabels; + + private static readonly byte[] LeLabelName = "le"u8.ToArray(); + + internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) + { + if (instanceLabelNames.Contains("le")) + { + throw new ArgumentException("'le' is a reserved label name"); + } + + _buckets = buckets ?? DefaultBuckets; + + if (_buckets.Length == 0) + { + throw new ArgumentException("Histogram must have at least one bucket"); + } + + if (!double.IsPositiveInfinity(_buckets[_buckets.Length - 1])) + { + _buckets = [.. _buckets, double.PositiveInfinity]; + } + + for (int i = 1; i < _buckets.Length; i++) + { + if (_buckets[i] <= _buckets[i - 1]) + { + throw new ArgumentException("Bucket values must be increasing"); + } + } + + _leLabels = new CanonicalLabel[_buckets.Length]; + for (var i = 0; i < _buckets.Length; i++) + { + _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, _buckets[i]); + } + +#if NET7_0_OR_GREATER + if (Avx.IsSupported) + { + _bucketsAlignmentBuffer = GC.AllocateUninitializedArray(_buckets.Length + (AvxAlignBytes / sizeof(double)), pinned: true); + + unsafe + { + var pointer = (nuint)Unsafe.AsPointer(ref _bucketsAlignmentBuffer[0]); + var pointerTooFarByBytes = pointer % AvxAlignBytes; + var bytesUntilNextAlignedPosition = (AvxAlignBytes - pointerTooFarByBytes) % AvxAlignBytes; + + if (bytesUntilNextAlignedPosition % sizeof(double) != 0) + throw new Exception("Unreachable code reached - all double[] allocations are expected to be at least 8-aligned."); + + _bucketsAlignmentBufferOffset = (int)(bytesUntilNextAlignedPosition / sizeof(double)); + } + + Array.Copy(_buckets, 0, _bucketsAlignmentBuffer, _bucketsAlignmentBufferOffset, _buckets.Length); + } + else + { + _bucketsAlignmentBuffer = []; + } +#endif + } + + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + } + + public sealed class Child : ChildBase, IHistogram + { + internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) + { + Parent = parent; + + _bucketCounts = new ThreadSafeLong[Parent._buckets.Length]; + + _exemplars = new ObservedExemplar[Parent._buckets.Length]; + for (var i = 0; i < Parent._buckets.Length; i++) + { + _exemplars[i] = ObservedExemplar.Empty; + } + } + + internal new readonly Histogram Parent; + + private ThreadSafeDouble _sum = new(0.0D); + private readonly ThreadSafeLong[] _bucketCounts; + private static readonly byte[] SumSuffix = "sum"u8.ToArray(); + private static readonly byte[] CountSuffix = "count"u8.ToArray(); + private static readonly byte[] BucketSuffix = "bucket"u8.ToArray(); + private readonly ObservedExemplar[] _exemplars; + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) + { + // We output sum. + // We output count. + // We output each bucket in order of increasing upper bound. + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + _sum.Value, + ObservedExemplar.Empty, + SumSuffix, + cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + Count, + ObservedExemplar.Empty, + CountSuffix, + cancel); + + var cumulativeCount = 0L; + + for (var i = 0; i < _bucketCounts.Length; i++) + { + var exemplar = BorrowExemplar(ref _exemplars[i]); + + cumulativeCount += _bucketCounts[i].Value; + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + Parent._leLabels[i], + cumulativeCount, + exemplar, + BucketSuffix, + cancel); + + ReturnBorrowedExemplar(ref _exemplars[i], exemplar); + } + } + + public double Sum => _sum.Value; + + public long Count + { + get + { + long total = 0; + + foreach (var count in _bucketCounts) + total += count.Value; + + return total; + } + } + + public void Observe(double val, Exemplar? exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); + + public void Observe(double val) => Observe(val, 1); + + public void Observe(double val, long count) => ObserveInternal(val, count, null); + + private void ObserveInternal(double val, long count, Exemplar? exemplar) + { + if (double.IsNaN(val)) + { + return; + } + + exemplar ??= GetDefaultExemplar(val); + + var bucketIndex = GetBucketIndex(val); + + _bucketCounts[bucketIndex].Add(count); + + if (exemplar?.Length > 0) + RecordExemplar(exemplar, ref _exemplars[bucketIndex], val); + + _sum.Add(val * count); + + Publish(); + } + + private int GetBucketIndex(double val) + { +#if NET7_0_OR_GREATER + if (Avx.IsSupported) + return GetBucketIndexAvx(val); +#endif + + for (int i = 0; i < Parent._buckets.Length; i++) + { + if (val <= Parent._buckets[i]) + return i; + } + + throw new Exception("Unreachable code reached."); + } + +#if NET7_0_OR_GREATER + /// + /// AVX allows us to perform 4 comparisons at the same time when finding the right bucket to increment. + /// The total speedup is not 4x due to various overheads but it's still 10-30% (more for wider histograms). + /// + private unsafe int GetBucketIndexAvx(double val) + { + // AVX operates on vectors of N buckets, so if the total is not divisible by N we need to check some of them manually. + var remaining = Parent._buckets.Length % Vector256.Count; + + for (int i = 0; i < Parent._buckets.Length - remaining; i += Vector256.Count) + { + // The buckets are permanently pinned, no need to re-pin them here. + var boundPointer = (double*)Unsafe.AsPointer(ref Parent._bucketsAlignmentBuffer[Parent._bucketsAlignmentBufferOffset + i]); + var boundVector = Avx.LoadAlignedVector256(boundPointer); + + var valVector = Vector256.Create(val); + + var mask = Avx.CompareLessThanOrEqual(valVector, boundVector); + + // Condenses the mask vector into a 32-bit integer where one bit represents one vector element (so 1111000.. means "first 4 items true"). + var moveMask = Avx.MoveMask(mask); + + var indexInBlock = BitOperations.TrailingZeroCount(moveMask); + + if (indexInBlock == sizeof(int) * 8) + continue; // All bits are zero, so we did not find a match. + + return i + indexInBlock; + } + + for (int i = Parent._buckets.Length - remaining; i < Parent._buckets.Length; i++) + { + if (val <= Parent._buckets[i]) + return i; + } + + throw new Exception("Unreachable code reached."); + } +#endif + } + + internal override MetricType Type => MetricType.Histogram; + + public double Sum => Unlabelled.Sum; + public long Count => Unlabelled.Count; + public void Observe(double val) => Unlabelled.Observe(val, 1); + public void Observe(double val, long count) => Unlabelled.Observe(val, count); + public void Observe(double val, Exemplar? exemplar) => Unlabelled.Observe(val, exemplar); + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go + /// + /// Creates '' buckets, where the lowest bucket has an + /// upper bound of '' and each following bucket's upper bound is '' + /// times the previous bucket's upper bound. + /// + /// The function throws if '' is 0 or negative, if '' is 0 or negative, + /// or if '' is less than or equal 1. + /// + /// The upper bound of the lowest bucket. Must be positive. + /// The factor to increase the upper bound of subsequent buckets. Must be greater than 1. + /// The number of buckets to create. Must be positive. + public static double[] ExponentialBuckets(double start, double factor, int count) + { + if (count <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(count)}"); + if (start <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(start)}"); + if (factor <= 1) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a {nameof(factor)} greater than 1"); + + // The math we do can make it incur some tiny avoidable error due to floating point gremlins. + // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. + // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. + + var next = (decimal)start; + var buckets = new double[count]; + + for (var i = 0; i < buckets.Length; i++) + { + buckets[i] = (double)next; + next *= (decimal)factor; + } + + return buckets; + } + + // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go + /// + /// Creates '' buckets, where the lowest bucket has an + /// upper bound of '' and each following bucket's upper bound is the upper bound of the + /// previous bucket, incremented by '' + /// + /// The function throws if '' is 0 or negative. + /// + /// The upper bound of the lowest bucket. + /// The width of each bucket (distance between lower and upper bound). + /// The number of buckets to create. Must be positive. + public static double[] LinearBuckets(double start, double width, int count) + { + if (count <= 0) throw new ArgumentException($"{nameof(LinearBuckets)} needs a positive {nameof(count)}"); + + // The math we do can make it incur some tiny avoidable error due to floating point gremlins. + // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. + // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. + + var next = (decimal)start; + var buckets = new double[count]; + + for (var i = 0; i < buckets.Length; i++) + { + buckets[i] = (double)next; + next += (decimal)width; + } + + return buckets; + } + + /// + /// Divides each power of 10 into N divisions. + /// + /// The starting range includes 10 raised to this power. + /// The ranges end with 10 raised to this power (this no longer starts a new range). + /// How many divisions to divide each range into. + /// + /// For example, with startPower=-1, endPower=2, divisions=4 we would get: + /// 10^-1 == 0.1 which defines our starting range, giving buckets: 0.25, 0.5, 0.75, 1.0 + /// 10^0 == 1 which is the next range, giving buckets: 2.5, 5, 7.5, 10 + /// 10^1 == 10 which is the next range, giving buckets: 25, 50, 75, 100 + /// 10^2 == 100 which is the end and the top level of the preceding range. + /// Giving total buckets: 0.25, 0.5, 0.75, 1.0, 2.5, 5, 7.5, 10, 25, 50, 75, 100 + /// + public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, int divisions) + { + if (startPower >= endPower) + throw new ArgumentException($"{nameof(startPower)} must be less than {nameof(endPower)}.", nameof(startPower)); + + if (divisions <= 0) + throw new ArgumentOutOfRangeException($"{nameof(divisions)} must be a positive integer.", nameof(divisions)); + + var buckets = new List(); + + for (var powerOfTen = startPower; powerOfTen < endPower; powerOfTen++) + { + // This gives us the upper bound (the start of the next range). + var max = (decimal)Math.Pow(10, powerOfTen + 1); + + // Then we just divide it into N divisions and we are done! + for (var division = 0; division < divisions; division++) + { + var bucket = max / divisions * (division + 1); + + // The math we do can make it incur some tiny avoidable error due to floating point gremlins. + // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. + // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. + var candidate = (double)bucket; + + // Depending on the number of divisions, it may be that divisions from different powers overlap. + // For example, a division into 20 would include: + // 19th value in the 0th power: 9.5 (10/20*19=9.5) + // 1st value in the 1st power: 5 (100/20*1 = 5) + // To avoid this being a problem, we simply constrain all values to be increasing. + if (buckets.Any() && buckets.Last() >= candidate) + continue; // Skip this one, it is not greater. + + buckets.Add(candidate); + } + } + + return [.. buckets]; + } + + // sum + count + buckets + internal override int TimeseriesCount => ChildCount * (2 + _buckets.Length); +} \ No newline at end of file diff --git a/Prometheus/HistogramConfiguration.cs b/Prometheus/HistogramConfiguration.cs new file mode 100644 index 00000000..e4c449d9 --- /dev/null +++ b/Prometheus/HistogramConfiguration.cs @@ -0,0 +1,17 @@ +namespace Prometheus; + +public sealed class HistogramConfiguration : MetricConfiguration +{ + internal static readonly HistogramConfiguration Default = new HistogramConfiguration(); + + /// + /// Custom histogram buckets to use. If null, will use Histogram.DefaultBuckets. + /// + public double[]? Buckets { get; set; } + + /// + /// Allows you to configure how exemplars are applied to the published metric. + /// If null, inherits the exemplar behavior from the metric factory. + /// + public ExemplarBehavior? ExemplarBehavior { get; set; } +} diff --git a/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs b/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs new file mode 100644 index 00000000..2f80afad --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs @@ -0,0 +1,106 @@ +namespace Prometheus.HttpClientMetrics; + +/// +/// This base class performs the data management necessary to associate the correct labels and values +/// with HttpClient metrics, depending on the options the user has provided for the HttpClient metric handler. +/// +/// The following labels are supported: +/// 'method' (HTTP request method) +/// 'host' (The host name of HTTP request) +/// 'client' (The name of the HttpClient) +/// 'code' (HTTP response status code) +/// +internal abstract class HttpClientDelegatingHandlerBase : DelegatingHandler + where TCollector : class, ICollector + where TChild : class, ICollectorChild +{ + /// + /// The set of labels from among the defaults that this metric supports. + /// + protected abstract string[] DefaultLabels { get; } + + /// + /// The factory to use for creating the default metric for this middleware. + /// Not used if a custom metric is already provided in options. + /// + protected MetricFactory MetricFactory { get; } + + /// + /// Creates the default metric instance with the specified set of labels. + /// Only used if the caller does not provide a custom metric instance in the options. + /// + protected abstract TCollector CreateMetricInstance(string[] labelNames); + + // Internal only for tests. + internal readonly TCollector _metric; + + protected HttpClientDelegatingHandlerBase(HttpClientMetricsOptionsBase? options, TCollector? customMetric, HttpClientIdentity identity) + { + _identity = identity; + + MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); + + if (customMetric != null) + { + _metric = customMetric; + + ValidateNoUnexpectedLabelNames(); + } + else + { + _metric = CreateMetricInstance(HttpClientRequestLabelNames.All); + } + } + + private readonly HttpClientIdentity _identity; + + /// + /// Creates the metric child instance to use for measurements. + /// + /// + /// Internal for testing purposes. + /// + protected internal TChild CreateChild(HttpRequestMessage request, HttpResponseMessage? response) + { + if (!_metric.LabelNames.Any()) + return _metric.Unlabelled; + + var labelValues = new string[_metric.LabelNames.Length]; + + for (var i = 0; i < labelValues.Length; i++) + { + switch (_metric.LabelNames[i]) + { + case HttpClientRequestLabelNames.Method: + labelValues[i] = request.Method.Method; + break; + case HttpClientRequestLabelNames.Host: + labelValues[i] = request.RequestUri?.Host ?? ""; + break; + case HttpClientRequestLabelNames.Client: + labelValues[i] = _identity.Name; + break; + case HttpClientRequestLabelNames.Code: + labelValues[i] = response != null ? ((int)response.StatusCode).ToString() : ""; + break; + default: + // We validate the label set on initialization, so this is impossible. + throw new NotSupportedException($"Found unsupported label on metric: {_metric.LabelNames[i]}"); + } + } + + return _metric.WithLabels(labelValues); + } + + /// + /// If we use a custom metric, it should not have labels that are not among the defaults. + /// + private void ValidateNoUnexpectedLabelNames() + { + var allowedLabels = HttpClientRequestLabelNames.All; + var unexpected = _metric.LabelNames.Except(allowedLabels); + + if (unexpected.Any()) + throw new ArgumentException($"Provided custom HttpClient metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); + } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs b/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs new file mode 100644 index 00000000..c1bb529c --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs @@ -0,0 +1,9 @@ +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientExporterOptions +{ + public HttpClientInProgressOptions InProgress { get; set; } = new HttpClientInProgressOptions(); + public HttpClientRequestCountOptions RequestCount { get; set; } = new HttpClientRequestCountOptions(); + public HttpClientRequestDurationOptions RequestDuration { get; set; } = new HttpClientRequestDurationOptions(); + public HttpClientResponseDurationOptions ResponseDuration { get; set; } = new HttpClientResponseDurationOptions(); +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientIdentity.cs b/Prometheus/HttpClientMetrics/HttpClientIdentity.cs new file mode 100644 index 00000000..7f8cde90 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientIdentity.cs @@ -0,0 +1,13 @@ +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientIdentity +{ + public static readonly HttpClientIdentity Default = new HttpClientIdentity("default"); + + public string Name { get; } + + public HttpClientIdentity(string name) + { + Name = name; + } +} diff --git a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs new file mode 100644 index 00000000..4624b02a --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs @@ -0,0 +1,25 @@ +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientInProgressHandler : HttpClientDelegatingHandlerBase, IGauge> +{ + public HttpClientInProgressHandler(HttpClientInProgressOptions? options, HttpClientIdentity identity) + : base(options, options?.Gauge, identity) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + using (CreateChild(request, null).TrackInProgress()) + { + // Returns when the response HEADERS are seen. + return await base.SendAsync(request, cancellationToken); + } + } + + protected override string[] DefaultLabels => HttpClientRequestLabelNames.KnownInAdvance; + + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( + "httpclient_requests_in_progress", + "Number of requests currently being executed by an HttpClient that have not yet received response headers. Value is decremented once response headers are received.", + labelNames); +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs b/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs new file mode 100644 index 00000000..e701914e --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs @@ -0,0 +1,9 @@ +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientInProgressOptions : HttpClientMetricsOptionsBase +{ + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Gauge { get; set; } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs b/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs new file mode 100644 index 00000000..f810d5c9 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs @@ -0,0 +1,12 @@ +namespace Prometheus.HttpClientMetrics; + +public abstract class HttpClientMetricsOptionsBase +{ + public bool Enabled { get; set; } = true; + + /// + /// Allows you to override the registry used to create the default metric instance. + /// Value is ignored if you specify a custom metric instance in the options. + /// + public CollectorRegistry? Registry { get; set; } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs b/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs new file mode 100644 index 00000000..57d2ee51 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs @@ -0,0 +1,31 @@ +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientRequestCountHandler : HttpClientDelegatingHandlerBase, ICounter> +{ + public HttpClientRequestCountHandler(HttpClientRequestCountOptions? options, HttpClientIdentity identity) + : base(options, options?.Counter, identity) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + + try + { + response = await base.SendAsync(request, cancellationToken); + return response; + } + finally + { + CreateChild(request, response).Inc(); + } + } + + protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; + + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( + "httpclient_requests_sent_total", + "Count of HTTP requests that have been completed by an HttpClient.", + labelNames); +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs b/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs new file mode 100644 index 00000000..c94bfd2d --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs @@ -0,0 +1,9 @@ +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientRequestCountOptions : HttpClientMetricsOptionsBase +{ + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Counter { get; set; } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs b/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs new file mode 100644 index 00000000..45b9c732 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs @@ -0,0 +1,40 @@ +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientRequestDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> +{ + public HttpClientRequestDurationHandler(HttpClientRequestDurationOptions? options, HttpClientIdentity identity) + : base(options, options?.Histogram, identity) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var stopWatch = ValueStopwatch.StartNew(); + + HttpResponseMessage? response = null; + + try + { + // We measure until SendAsync returns - which is when the response HEADERS are seen. + // The response body may continue streaming for a long time afterwards, which this does not measure. + response = await base.SendAsync(request, cancellationToken); + return response; + } + finally + { + CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); + } + } + + protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; + + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( + "httpclient_request_duration_seconds", + "Duration histogram of HTTP requests performed by an HttpClient.", + labelNames, + new HistogramConfiguration + { + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), + }); +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs b/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs new file mode 100644 index 00000000..84c91252 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs @@ -0,0 +1,9 @@ +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientRequestDurationOptions : HttpClientMetricsOptionsBase +{ + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Histogram { get; set; } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs b/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs new file mode 100644 index 00000000..2684fd77 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs @@ -0,0 +1,29 @@ +namespace Prometheus.HttpClientMetrics; + +/// +/// Label names reserved for the use by the HttpClient metrics. +/// +public static class HttpClientRequestLabelNames +{ + public const string Method = "method"; + public const string Host = "host"; + public const string Client = "client"; + public const string Code = "code"; + + public static readonly string[] All = + { + Method, + Host, + Client, + Code + }; + + // The labels known before receiving the response. + // Everything except the response status code, basically. + public static readonly string[] KnownInAdvance = + { + Method, + Host, + Client + }; +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs b/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs new file mode 100644 index 00000000..69ff3529 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs @@ -0,0 +1,141 @@ +using System.Net.Http.Headers; + +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientResponseDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> +{ + public HttpClientResponseDurationHandler(HttpClientResponseDurationOptions? options, HttpClientIdentity identity) + : base(options, options?.Histogram, identity) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var stopWatch = ValueStopwatch.StartNew(); + + var response = await base.SendAsync(request, cancellationToken); + + Stream oldStream = await response.Content.ReadAsStreamAsync(); + + Wrap(response, oldStream, delegate + { + CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); + }); + + return response; + } + + protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; + + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( + "httpclient_response_duration_seconds", + "Duration histogram of HTTP requests performed by an HttpClient, measuring the duration until the HTTP response finished being processed.", + labelNames, + new HistogramConfiguration + { + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), + }); + + private void Wrap(HttpResponseMessage response, Stream oldStream, Action onEndOfStream) + { + var newContent = new StreamContent(new EndOfStreamDetectingStream(oldStream, onEndOfStream)); + + var oldHeaders = response.Content.Headers; + var newHeaders = newContent.Headers; + +#if NET6_0_OR_GREATER + foreach (KeyValuePair header in oldHeaders.NonValidated) + { + if (header.Value.Count > 1) + { + newHeaders.TryAddWithoutValidation(header.Key, header.Value); + } + else + { + newHeaders.TryAddWithoutValidation(header.Key, header.Value.ToString()); + } + } +#else + foreach (var header in oldHeaders) + { + newHeaders.TryAddWithoutValidation(header.Key, header.Value); + } +#endif + + response.Content = newContent; + } + + private sealed class EndOfStreamDetectingStream : Stream + { + public EndOfStreamDetectingStream(Stream inner, Action onEndOfStream) + { + _inner = inner; + _onEndOfStream = onEndOfStream; + } + + private readonly Stream _inner; + private readonly Action _onEndOfStream; + private int _sawEndOfStream = 0; + + public override void Flush() => _inner.Flush(); + + public override int Read(byte[] buffer, int offset, int count) + { + var read = _inner.Read(buffer, offset, count); + + if (read == 0 && buffer.Length != 0) + { + SignalCompletion(); + } + + return read; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return buffer.Length == 0 + ? _inner.ReadAsync(buffer, offset, count, cancellationToken) + : ReadAsyncCore(this, _inner.ReadAsync(buffer, offset, count, cancellationToken)); + + static async Task ReadAsyncCore(EndOfStreamDetectingStream stream, Task readTask) + { + int read = await readTask; + + if (read == 0) + { + stream.SignalCompletion(); + } + + return read; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + SignalCompletion(); + + _inner.Dispose(); + } + } + + private void SignalCompletion() + { + if (Interlocked.Exchange(ref _sawEndOfStream, 1) == 0) + { + _onEndOfStream(); + } + } + + public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin); + public override void SetLength(long value) => _inner.SetLength(value); + public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count); + public override bool CanRead => _inner.CanRead; + public override bool CanSeek => _inner.CanSeek; + public override bool CanWrite => _inner.CanWrite; + public override long Length => _inner.Length; + public override long Position { get => _inner.Position; set => _inner.Position = value; } + } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs b/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs new file mode 100644 index 00000000..39f53bb5 --- /dev/null +++ b/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs @@ -0,0 +1,9 @@ +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientResponseDurationOptions : HttpClientMetricsOptionsBase +{ + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Histogram { get; set; } +} \ No newline at end of file diff --git a/Prometheus/HttpClientMetricsExtensions.cs b/Prometheus/HttpClientMetricsExtensions.cs new file mode 100644 index 00000000..ab529d0f --- /dev/null +++ b/Prometheus/HttpClientMetricsExtensions.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Prometheus.HttpClientMetrics; + +namespace Prometheus; + +public static class HttpClientMetricsExtensions +{ + /// + /// Configures the HttpClient pipeline to collect Prometheus metrics. + /// + public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, Action configure) + { + var options = new HttpClientExporterOptions(); + + configure?.Invoke(options); + + builder.UseHttpClientMetrics(options); + + return builder; + } + + /// + /// Configures the HttpClient pipeline to collect Prometheus metrics. + /// + public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, HttpClientExporterOptions? options = null) + { + options ??= new HttpClientExporterOptions(); + + var identity = new HttpClientIdentity(builder.Name); + + if (options.InProgress.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientInProgressHandler(options.InProgress, identity)); + } + + if (options.RequestCount.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientRequestCountHandler(options.RequestCount, identity)); + } + + if (options.RequestDuration.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientRequestDurationHandler(options.RequestDuration, identity)); + } + + if (options.ResponseDuration.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); + } + + return builder; + } + + /// + /// Configures the HttpMessageHandler pipeline to collect Prometheus metrics. + /// + public static HttpMessageHandlerBuilder UseHttpClientMetrics(this HttpMessageHandlerBuilder builder, HttpClientExporterOptions? options = null) + { + options ??= new HttpClientExporterOptions(); + + var identity = new HttpClientIdentity(builder.Name); + + if (options.InProgress.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientInProgressHandler(options.InProgress, identity)); + } + + if (options.RequestCount.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientRequestCountHandler(options.RequestCount, identity)); + } + + if (options.RequestDuration.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientRequestDurationHandler(options.RequestDuration, identity)); + } + + if (options.ResponseDuration.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); + } + + return builder; + } + + /// + /// Configures the service container to collect Prometheus metrics from all registered HttpClients. + /// + public static IServiceCollection UseHttpClientMetrics(this IServiceCollection services, HttpClientExporterOptions? options = null) + { + return services.ConfigureAll((HttpClientFactoryOptions optionsToConfigure) => + { + optionsToConfigure.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.UseHttpClientMetrics(options); + }); + }); + } +} \ No newline at end of file diff --git a/Prometheus/ICollector.cs b/Prometheus/ICollector.cs new file mode 100644 index 00000000..6b162fbc --- /dev/null +++ b/Prometheus/ICollector.cs @@ -0,0 +1,23 @@ +namespace Prometheus; + +/// +/// Child-type-specific interface implemented by all collectors, used to enable substitution in test code. +/// +public interface ICollector : ICollector + where TChild : ICollectorChild +{ + TChild Unlabelled { get; } + TChild WithLabels(params string[] labelValues); + TChild WithLabels(ReadOnlyMemory labelValues); + TChild WithLabels(ReadOnlySpan labelValues); +} + +/// +/// Interface implemented by all collectors, used to enable substitution in test code. +/// +public interface ICollector +{ + string Name { get; } + string Help { get; } + string[] LabelNames { get; } +} diff --git a/Prometheus/ICollectorChild.cs b/Prometheus/ICollectorChild.cs new file mode 100644 index 00000000..c7dc32e6 --- /dev/null +++ b/Prometheus/ICollectorChild.cs @@ -0,0 +1,8 @@ +namespace Prometheus; + +/// +/// Interface shared by all labelled collector children. +/// +public interface ICollectorChild +{ +} diff --git a/Prometheus/ICollectorRegistry.cs b/Prometheus/ICollectorRegistry.cs new file mode 100644 index 00000000..bca0bf06 --- /dev/null +++ b/Prometheus/ICollectorRegistry.cs @@ -0,0 +1,16 @@ +namespace Prometheus; + +/// +/// Allows for substitution of CollectorRegistry in tests. +/// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code. +/// +public interface ICollectorRegistry +{ + void AddBeforeCollectCallback(Action callback); + void AddBeforeCollectCallback(Func callback); + + IEnumerable> StaticLabels { get; } + void SetStaticLabels(IDictionary labels); + + Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.PrometheusText, CancellationToken cancel = default); +} diff --git a/Prometheus/ICounter.cs b/Prometheus/ICounter.cs new file mode 100644 index 00000000..658e0033 --- /dev/null +++ b/Prometheus/ICounter.cs @@ -0,0 +1,34 @@ +namespace Prometheus; + +public interface ICounter : ICollectorChild +{ + /// + /// Increment a counter by 1. + /// + void Inc(double increment = 1.0); + + /// + /// Increment a counter by 1. + /// + /// + /// A set of labels representing an exemplar, created using Exemplar.From(). + /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. + /// Pass Exemplar.None to explicitly record an observation without an exemplar. + /// + void Inc(Exemplar? exemplar); + + /// + /// Increment a counter. + /// + /// The increment. + /// + /// A set of labels representing an exemplar, created using Exemplar.From(). + /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. + /// Pass Exemplar.None to explicitly record an observation without an exemplar. + /// + void Inc(double increment, Exemplar? exemplar); + + void IncTo(double targetValue); + + double Value { get; } +} diff --git a/Prometheus/IDelayer.cs b/Prometheus/IDelayer.cs new file mode 100644 index 00000000..7c121534 --- /dev/null +++ b/Prometheus/IDelayer.cs @@ -0,0 +1,10 @@ +namespace Prometheus; + +/// +/// Abstraction over Task.Delay() to allow custom delay logic to be injected in tests. +/// +internal interface IDelayer +{ + Task Delay(TimeSpan duration); + Task Delay(TimeSpan duration, CancellationToken cancel); +} diff --git a/Prometheus/IGauge.cs b/Prometheus/IGauge.cs new file mode 100644 index 00000000..2e7ebda7 --- /dev/null +++ b/Prometheus/IGauge.cs @@ -0,0 +1,11 @@ +namespace Prometheus; + +public interface IGauge : ICollectorChild +{ + void Inc(double increment = 1); + void Set(double val); + void Dec(double decrement = 1); + void IncTo(double targetValue); + void DecTo(double targetValue); + double Value { get; } +} diff --git a/Prometheus/IHistogram.cs b/Prometheus/IHistogram.cs new file mode 100644 index 00000000..72508f39 --- /dev/null +++ b/Prometheus/IHistogram.cs @@ -0,0 +1,34 @@ +namespace Prometheus; + +public interface IHistogram : IObserver +{ + /// + /// Observe multiple events with a given value. + /// + /// Intended to support high frequency or batch processing use cases utilizing pre-aggregation. + /// + /// Measured value. + /// Number of observations with this value. + void Observe(double val, long count); + + /// + /// Observe an event with an exemplar + /// + /// Measured value. + /// + /// A set of labels representing an exemplar, created using Exemplar.From(). + /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. + /// Pass Exemplar.None to explicitly record an observation without an exemplar. + /// + void Observe(double val, Exemplar? exemplar); + + /// + /// Gets the sum of all observed events. + /// + double Sum { get; } + + /// + /// Gets the count of all observed events. + /// + long Count { get; } +} diff --git a/Prometheus/IManagedLifetimeMetricFactory.cs b/Prometheus/IManagedLifetimeMetricFactory.cs new file mode 100644 index 00000000..ee9f9c4e --- /dev/null +++ b/Prometheus/IManagedLifetimeMetricFactory.cs @@ -0,0 +1,42 @@ +namespace Prometheus; + +/// +/// A metric factory for creating metrics that use a managed lifetime, whereby the metric may +/// be deleted based on logic other than disposal or similar explicit deletion. +/// +/// +/// The lifetime management logic is associated with a metric handle. Calling CreateXyz() with equivalent identity parameters will return +/// the same handle. However, using multiple factories will create independent handles (which will delete the same metric independently). +/// +public interface IManagedLifetimeMetricFactory +{ + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[]? labelNames = null, CounterConfiguration? configuration = null); + + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? labelNames = null, GaugeConfiguration? configuration = null); + + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? labelNames = null, HistogramConfiguration? configuration = null); + + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? labelNames = null, SummaryConfiguration? configuration = null); + + /// + /// Returns a new metric factory that will add the specified labels to any metrics created using it. + /// Different instances returned for the same labels are equivalent and any metrics created via them share their lifetimes. + /// + IManagedLifetimeMetricFactory WithLabels(IDictionary labels); +} diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs new file mode 100644 index 00000000..0112589e --- /dev/null +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -0,0 +1,263 @@ +namespace Prometheus; + +/// +/// Handle to a metric with a lease-extended lifetime, enabling the metric to be accessed and its lifetime to be controlled. +/// Each label combination is automatically deleted N seconds after the last lease on that label combination expires. +/// +/// +/// When creating leases, prefer the overload that takes a ReadOnlySpan because it avoids +/// allocating a string array if the metric instance you are leasing is already alive. +/// +public interface IManagedLifetimeMetricHandle + where TMetricInterface : ICollectorChild +{ + #region Lease(string[]) + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + IDisposable AcquireLease(out TMetricInterface metric, params string[] labelValues); + + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// The lease is returned as a stack-only struct, which is faster than the IDisposable version. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + RefLease AcquireRefLease(out TMetricInterface metric, params string[] labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, params string[] labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// Passes a given argument to the callback. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, TArg arg, params string[] labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + Task WithLeaseAsync(Func func, params string[] labelValues); + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + TResult WithLease(Func func, params string[] labelValues); + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + Task WithLeaseAsync(Func> action, params string[] labelValues); + #endregion + + #region Lease(ReadOnlyMemory) + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + IDisposable AcquireLease(out TMetricInterface metric, ReadOnlyMemory labelValues); + + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// The lease is returned as a stack-only struct, which is faster than the IDisposable version. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlyMemory labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, ReadOnlyMemory labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// Passes a given argument to the callback. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues); + + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + TResult WithLease(Func func, ReadOnlyMemory labelValues); + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues); + #endregion + + #region Lease(ReadOnlySpan) + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + IDisposable AcquireLease(out TMetricInterface metric, ReadOnlySpan labelValues); + + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// The lease is returned as a stack-only struct, which is faster than the IDisposable version. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlySpan labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, ReadOnlySpan labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// Passes a given argument to the callback. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, TArg arg, ReadOnlySpan labelValues); + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + TResult WithLease(Func func, ReadOnlySpan labelValues); + #endregion + + /// + /// Returns a metric instance that automatically extends the lifetime of the timeseries whenever the value is changed. + /// This is equivalent to taking a lease for every update to the value, and immediately releasing the lease. + /// + /// This is useful if the caller is lifetime-management-agnostic code that is not aware of the possibility to extend metric lifetime via leases. + /// Do not use this if you can use explicit leases instead, as this is considerably less efficient. + /// + ICollector WithExtendLifetimeOnUse(); +} diff --git a/Prometheus/IMetricFactory.cs b/Prometheus/IMetricFactory.cs new file mode 100644 index 00000000..b58fde35 --- /dev/null +++ b/Prometheus/IMetricFactory.cs @@ -0,0 +1,48 @@ +using System.ComponentModel; + +namespace Prometheus; + +/// +/// Allows for substitution of MetricFactory in tests. +/// You cannot provide your own implementation to prometheus-net code, only to your own code. +/// +public interface IMetricFactory +{ + // These require you to allocate a Configuration for each instance, which can be wasteful because often the only thing that differs is the label names. + // We will mark them as non-browsable to discourage their use. They still work, so they are not obsolete or anything like that. Just discouraged. + [EditorBrowsable(EditorBrowsableState.Never)] + Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null); + [EditorBrowsable(EditorBrowsableState.Never)] + Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null); + [EditorBrowsable(EditorBrowsableState.Never)] + Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null); + [EditorBrowsable(EditorBrowsableState.Never)] + Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null); + + // These allow you to reuse a Configuration and only provide the label names. The reduced memory allocations can make a difference in high performance scenarios. + // If label names are provided in both, they must match. Otherwise, label names in the Configuration object may be null. + Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null); + Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null); + Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null); + Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null); + + /// + /// Returns a new metric factory that will add the specified labels to any metrics created using it. + /// + IMetricFactory WithLabels(IDictionary labels); + + /// + /// Returns a factory that creates metrics with a managed lifetime. + /// + /// + /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. + /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. + /// + IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter); + + /// + /// Allows you to configure how exemplars are applied to published metrics. If null, uses default behavior (see ). + /// This is inherited by all metrics by default, although may be overridden in the configuration of an individual metric. + /// + ExemplarBehavior? ExemplarBehavior { get; set; } +} diff --git a/Prometheus/IMetricServer.cs b/Prometheus/IMetricServer.cs new file mode 100644 index 00000000..34652a49 --- /dev/null +++ b/Prometheus/IMetricServer.cs @@ -0,0 +1,25 @@ +namespace Prometheus; + +/// +/// A metric server exposes a Prometheus metric exporter endpoint in the background, +/// operating independently and serving metrics until it is instructed to stop. +/// +public interface IMetricServer : IDisposable +{ + /// + /// Starts serving metrics. + /// + /// Returns the same instance that was called (for fluent-API-style chaining). + /// + IMetricServer Start(); + + /// + /// Instructs the metric server to stop and returns a task you can await for it to stop. + /// + Task StopAsync(); + + /// + /// Instructs the metric server to stop and waits for it to stop. + /// + void Stop(); +} diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs new file mode 100644 index 00000000..c1701503 --- /dev/null +++ b/Prometheus/IMetricsSerializer.cs @@ -0,0 +1,37 @@ +namespace Prometheus; + +/// +/// The only purpose this serves is to warn the developer when he might be accidentally introducing +/// new serialization-time relationships. The serialization code is very tied to the text format and +/// not intended to be a generic serialization mechanism. +/// +internal interface IMetricsSerializer +{ + /// + /// Writes the lines that declare the metric family. + /// + ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel); + + /// + /// Writes out a single metric point with a floating point value. + /// + ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, + double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel); + + /// + /// Writes out a single metric point with an integer value. + /// + ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, + long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel); + + /// + /// Writes out terminal lines + /// + ValueTask WriteEnd(CancellationToken cancel); + + /// + /// Flushes any pending buffers. Always call this after all your write calls. + /// + Task FlushAsync(CancellationToken cancel); +} \ No newline at end of file diff --git a/Prometheus/INotifyLeaseEnded.cs b/Prometheus/INotifyLeaseEnded.cs new file mode 100644 index 00000000..d402a518 --- /dev/null +++ b/Prometheus/INotifyLeaseEnded.cs @@ -0,0 +1,6 @@ +namespace Prometheus; + +internal interface INotifyLeaseEnded +{ + void OnLeaseEnded(object child, ChildLifetimeInfo lifetime); +} diff --git a/Prometheus/IObserver.cs b/Prometheus/IObserver.cs new file mode 100644 index 00000000..a5ffd912 --- /dev/null +++ b/Prometheus/IObserver.cs @@ -0,0 +1,12 @@ +namespace Prometheus; + +/// +/// Implemented by metric types that observe individual events with specific values. +/// +public interface IObserver : ICollectorChild +{ + /// + /// Observes a single event with the given value. + /// + void Observe(double val); +} \ No newline at end of file diff --git a/Prometheus/ISummary.cs b/Prometheus/ISummary.cs new file mode 100644 index 00000000..a775d53c --- /dev/null +++ b/Prometheus/ISummary.cs @@ -0,0 +1,5 @@ +namespace Prometheus; + +public interface ISummary : IObserver +{ +} diff --git a/Prometheus/ITimer.cs b/Prometheus/ITimer.cs new file mode 100644 index 00000000..3334e836 --- /dev/null +++ b/Prometheus/ITimer.cs @@ -0,0 +1,14 @@ +namespace Prometheus; + +/// +/// A timer that can be used to observe a duration of elapsed time. +/// +/// The observation is made either when ObserveDuration is called or when the instance is disposed of. +/// +public interface ITimer : IDisposable +{ + /// + /// Observes the duration (in seconds) and returns the observed value. + /// + TimeSpan ObserveDuration(); +} diff --git a/Prometheus/LabelEnrichingAutoLeasingMetric.cs b/Prometheus/LabelEnrichingAutoLeasingMetric.cs new file mode 100644 index 00000000..b7ad7f50 --- /dev/null +++ b/Prometheus/LabelEnrichingAutoLeasingMetric.cs @@ -0,0 +1,82 @@ +using System.Buffers; + +namespace Prometheus; + +internal sealed class LabelEnrichingAutoLeasingMetric : ICollector + where TMetric : ICollectorChild +{ + public LabelEnrichingAutoLeasingMetric(ICollector inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly ICollector _inner; + private readonly string[] _enrichWithLabelValues; + + public TMetric Unlabelled + { + get + { + // If we are not provided any custom label values, we can be pretty sure the label values are not going to change + // between calls, so reuse a buffer to avoid allocations when passing the data to the inner instance. + var buffer = ArrayPool.Shared.Rent(_enrichWithLabelValues.Length); + + try + { + _enrichWithLabelValues.CopyTo(buffer, 0); + var finalLabelValues = buffer.AsSpan(0, _enrichWithLabelValues.Length); + + return _inner.WithLabels(finalLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } + + public string Name => _inner.Name; + public string Help => _inner.Help; + + // We do not display the enriched labels, they are transparent - this is only the instance-specific label names. + public string[] LabelNames => _inner.LabelNames; + + public TMetric WithLabels(params string[] labelValues) + { + // The caller passing us string[] does not signal that the allocation is not needed - in all likelihood it is. + // However, we do not want to allocate two arrays here (because we need to concatenate as well) so instead we + // use the reusable-buffer overload to avoid at least one of the allocations. + + return WithLabels(labelValues.AsSpan()); + } + + public TMetric WithLabels(ReadOnlyMemory labelValues) + { + // The caller passing us ReadOnlyMemory does not signal that the allocation is not needed - in all likelihood it is. + // However, we do not want to allocate two arrays here (because we need to concatenate as well) so instead we + // use the reusable-buffer overload to avoid at least one of the allocations. + + return WithLabels(labelValues.Span); + } + + public TMetric WithLabels(ReadOnlySpan labelValues) + { + // The ReadOnlySpan overload suggests that the label values may already be known to the metric, + // so we should strongly avoid allocating memory here. Thus we copy everything to a reusable buffer. + var buffer = ArrayPool.Shared.Rent(_enrichWithLabelValues.Length + labelValues.Length); + + try + { + _enrichWithLabelValues.CopyTo(buffer, 0); + labelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + var finalLabelValues = buffer.AsSpan(0, _enrichWithLabelValues.Length + labelValues.Length); + + return _inner.WithLabels(finalLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs new file mode 100644 index 00000000..e3ccad5e --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs @@ -0,0 +1,201 @@ +using System.Buffers; + +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeCounter : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeCounter(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + // Internal for manipulation during testing. + internal readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + #region Lease(string[]) + public IDisposable AcquireLease(out ICounter metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out ICounter metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out ICounter metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out ICounter metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; + } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out ICounter metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out ICounter metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs new file mode 100644 index 00000000..2f272b65 --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs @@ -0,0 +1,200 @@ +using System.Buffers; + +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeGauge : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeGauge(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + #region Lease(string[]) + public IDisposable AcquireLease(out IGauge metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out IGauge metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out IGauge metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out IGauge metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; + } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out IGauge metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out IGauge metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs new file mode 100644 index 00000000..6a645cd5 --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs @@ -0,0 +1,200 @@ +using System.Buffers; + +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeHistogram : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeHistogram(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + #region Lease(string[]) + public IDisposable AcquireLease(out IHistogram metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out IHistogram metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out IHistogram metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out IHistogram metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; + } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out IHistogram metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out IHistogram metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs new file mode 100644 index 00000000..11d6d7a7 --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs @@ -0,0 +1,247 @@ +namespace Prometheus; + +/// +/// Applies a set of static labels to lifetime-managed metrics. Multiple instances are functionally equivalent for the same label set. +/// +internal sealed class LabelEnrichingManagedLifetimeMetricFactory : IManagedLifetimeMetricFactory +{ + public LabelEnrichingManagedLifetimeMetricFactory(ManagedLifetimeMetricFactory inner, IDictionary enrichWithLabels) + { + _inner = inner; + + // We just need the items to be consistently ordered between equivalent instances but it does not actually matter what the order is. + _labels = enrichWithLabels.OrderBy(x => x.Key, StringComparer.Ordinal).ToList(); + + _enrichWithLabelNames = enrichWithLabels.Select(x => x.Key).ToArray(); + _enrichWithLabelValues = enrichWithLabels.Select(x => x.Value).ToArray(); + } + + private readonly ManagedLifetimeMetricFactory _inner; + + // This is an ordered list because labels have specific order. + private readonly IReadOnlyList> _labels; + + // Cache the names/values to enrich with, for reuse. + // We could perhaps improve even further via StringSequence but that requires creating separate internal APIs so can be a future optimization. + private readonly string[] _enrichWithLabelNames; + private readonly string[] _enrichWithLabelValues; + + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[]? instanceLabelNames, CounterConfiguration? configuration) + { + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); + var innerHandle = _inner.CreateCounter(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + + _countersLock.EnterReadLock(); + + try + { + if (_counters.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _countersLock.ExitReadLock(); + } + + var instance = CreateCounterCore(innerHandle); + + _countersLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_counters.TryAdd(innerHandle, instance)) + return instance; + + return _counters[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_counters.TryGetValue(innerHandle, out var existing)) + return existing; + + _counters.Add(innerHandle, instance); + return instance; +#endif + } + finally + { + _countersLock.ExitWriteLock(); + } + } + + private LabelEnrichingManagedLifetimeCounter CreateCounterCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeCounter(inner, _enrichWithLabelValues); + + private readonly Dictionary, LabelEnrichingManagedLifetimeCounter> _counters = new(); + private readonly ReaderWriterLockSlim _countersLock = new(); + + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) + { + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); + var innerHandle = _inner.CreateGauge(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + + _gaugesLock.EnterReadLock(); + + try + { + if (_gauges.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _gaugesLock.ExitReadLock(); + } + + var instance = CreateGaugeCore(innerHandle); + + _gaugesLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_gauges.TryAdd(innerHandle, instance)) + return instance; + + return _gauges[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_gauges.TryGetValue(innerHandle, out var existing)) + return existing; + + _gauges.Add(innerHandle, instance); + return instance; +#endif + } + finally + { + _gaugesLock.ExitWriteLock(); + } + } + + private LabelEnrichingManagedLifetimeGauge CreateGaugeCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeGauge(inner, _enrichWithLabelValues); + private readonly Dictionary, LabelEnrichingManagedLifetimeGauge> _gauges = new(); + private readonly ReaderWriterLockSlim _gaugesLock = new(); + + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) + { + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); + var innerHandle = _inner.CreateHistogram(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + + _histogramsLock.EnterReadLock(); + + try + { + if (_histograms.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _histogramsLock.ExitReadLock(); + } + + var instance = CreateHistogramCore(innerHandle); + + _histogramsLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_histograms.TryAdd(innerHandle, instance)) + return instance; + + return _histograms[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_histograms.TryGetValue(innerHandle, out var existing)) + return existing; + + _histograms.Add(innerHandle, instance); + return instance; +#endif + } + finally + { + _histogramsLock.ExitWriteLock(); + } + } + + private LabelEnrichingManagedLifetimeHistogram CreateHistogramCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeHistogram(inner, _enrichWithLabelValues); + private readonly Dictionary, LabelEnrichingManagedLifetimeHistogram> _histograms = new(); + private readonly ReaderWriterLockSlim _histogramsLock = new(); + + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) + { + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); + var innerHandle = _inner.CreateSummary(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + + _summariesLock.EnterReadLock(); + + try + { + if (_summaries.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _summariesLock.ExitReadLock(); + } + + var instance = CreateSummaryCore(innerHandle); + + _summariesLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_summaries.TryAdd(innerHandle, instance)) + return instance; + + return _summaries[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_summaries.TryGetValue(innerHandle, out var existing)) + return existing; + + _summaries.Add(innerHandle, instance); + return instance; +#endif + } + finally + { + _summariesLock.ExitWriteLock(); + } + } + + private LabelEnrichingManagedLifetimeSummary CreateSummaryCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeSummary(inner, _enrichWithLabelValues); + private readonly Dictionary, LabelEnrichingManagedLifetimeSummary> _summaries = new(); + private readonly ReaderWriterLockSlim _summariesLock = new(); + + public IManagedLifetimeMetricFactory WithLabels(IDictionary labels) + { + var combinedLabels = _labels.Concat(labels).ToDictionary(x => x.Key, x => x.Value); + + // Inner factory takes care of applying the correct ordering for labels. + return _inner.WithLabels(combinedLabels); + } + + private string[] WithEnrichedLabelNames(string[] instanceLabelNames) + { + // Enrichment labels always go first when we are communicating with the inner factory. + return _enrichWithLabelNames.Concat(instanceLabelNames).ToArray(); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs new file mode 100644 index 00000000..796045fb --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs @@ -0,0 +1,200 @@ +using System.Buffers; + +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeSummary : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeSummary(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + #region Lease(string[]) + public IDisposable AcquireLease(out ISummary metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out ISummary metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out ISummary metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out ISummary metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; + } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out ISummary metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out ISummary metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } +} diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs new file mode 100644 index 00000000..c3266e9f --- /dev/null +++ b/Prometheus/LabelSequence.cs @@ -0,0 +1,245 @@ +namespace Prometheus; + +/// +/// A sequence of metric label-name pairs. +/// +internal readonly struct LabelSequence : IEquatable +{ + public static readonly LabelSequence Empty = new(); + + public readonly StringSequence Names; + public readonly StringSequence Values; + + public int Length => Names.Length; + + private LabelSequence(StringSequence names, StringSequence values) + { + if (names.Length != values.Length) + throw new ArgumentException("The list of label values must have the same number of elements as the list of label names."); + + Names = names; + Values = values; + + _hashCode = CalculateHashCode(); + } + + public static LabelSequence From(StringSequence names, StringSequence values) + { + return new LabelSequence(names, values); + } + + public static LabelSequence From(IDictionary dictionary) + { + var names = new string[dictionary.Count]; + var values = new string[dictionary.Count]; + + var index = 0; + + foreach (var pair in dictionary) + { + names[index] = pair.Key; + values[index] = pair.Value; + index++; + } + + return new LabelSequence(StringSequence.From(names), StringSequence.From(values)); + } + + /// + /// Creates a new label sequence with some additional labels concatenated to the current sequence. + /// + public LabelSequence Concat(LabelSequence labels) + { + return new LabelSequence(Names.Concat(labels.Names), Values.Concat(labels.Values)); + } + + public bool TryGetLabelValue(string labelName, out string labelValue) + { + var nameEnumerator = Names.GetEnumerator(); + var valueEnumerator = Values.GetEnumerator(); + + for (var i = 0; i < Names.Length; i++) + { + if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); + if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); + + if (nameEnumerator.Current.Equals(labelName, StringComparison.Ordinal)) + { + labelValue = valueEnumerator.Current; + return true; + } + } + + labelValue = string.Empty; + return false; + } + + private static string EscapeLabelValue(string value) + { + return value + .Replace("\\", @"\\") + .Replace("\n", @"\n") + .Replace("\"", @"\"""); + } + + private static int GetEscapedLabelValueByteCount(string value) + { + var byteCount = PrometheusConstants.ExportEncoding.GetByteCount(value); + + foreach (var c in value) + { + if (c == '\\' || c == '\n' || c == '"') + byteCount++; + } + + return byteCount; + } + + /// + /// Serializes to the labelkey1="labelvalue1",labelkey2="labelvalue2" label string as bytes. + /// + public byte[] Serialize() + { + // Result is cached in child collector, though we still might be making many of these child collectors if they are not reused. + // Let's try to be efficient to avoid allocations if this gets called in a hot path. + + // First pass - calculate how many bytes we need to allocate. + var nameEnumerator = Names.GetEnumerator(); + var valueEnumerator = Values.GetEnumerator(); + + var byteCount = 0; + + for (var i = 0; i < Names.Length; i++) + { + if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); + if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); + + if (i != 0) + byteCount += TextSerializer.Comma.Length; + + byteCount += PrometheusConstants.ExportEncoding.GetByteCount(nameEnumerator.Current); + byteCount += TextSerializer.Equal.Length; + byteCount += TextSerializer.Quote.Length; + byteCount += GetEscapedLabelValueByteCount(valueEnumerator.Current); + byteCount += TextSerializer.Quote.Length; + } + + var bytes = new byte[byteCount]; + var index = 0; + + nameEnumerator = Names.GetEnumerator(); + valueEnumerator = Values.GetEnumerator(); + + for (var i = 0; i < Names.Length; i++) + { + if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); + if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); + +#if NET + if (i != 0) + { + TextSerializer.Comma.CopyTo(bytes.AsSpan(index)); + index += TextSerializer.Comma.Length; + } + + index += PrometheusConstants.ExportEncoding.GetBytes(nameEnumerator.Current, 0, nameEnumerator.Current.Length, bytes, index); + + TextSerializer.Equal.CopyTo(bytes.AsSpan(index)); + index += TextSerializer.Equal.Length; + + TextSerializer.Quote.CopyTo(bytes.AsSpan(index)); + index += TextSerializer.Quote.Length; + + var escapedLabelValue = EscapeLabelValue(valueEnumerator.Current); + index += PrometheusConstants.ExportEncoding.GetBytes(escapedLabelValue, 0, escapedLabelValue.Length, bytes, index); + + TextSerializer.Quote.CopyTo(bytes.AsSpan(index)); + index += TextSerializer.Quote.Length; +#else + if (i != 0) + { + Array.Copy(TextSerializer.Comma, 0, bytes, index, TextSerializer.Comma.Length); + index += TextSerializer.Comma.Length; + } + + index += PrometheusConstants.ExportEncoding.GetBytes(nameEnumerator.Current, 0, nameEnumerator.Current.Length, bytes, index); + + Array.Copy(TextSerializer.Equal, 0, bytes, index, TextSerializer.Equal.Length); + index += TextSerializer.Equal.Length; + + Array.Copy(TextSerializer.Quote, 0, bytes, index, TextSerializer.Quote.Length); + index += TextSerializer.Quote.Length; + + var escapedLabelValue = EscapeLabelValue(valueEnumerator.Current); + index += PrometheusConstants.ExportEncoding.GetBytes(escapedLabelValue, 0, escapedLabelValue.Length, bytes, index); + + Array.Copy(TextSerializer.Quote, 0, bytes, index, TextSerializer.Quote.Length); + index += TextSerializer.Quote.Length; +#endif + } + + if (index != byteCount) throw new Exception("API contract violation - we counted the same bytes twice but got different numbers."); + + return bytes; + } + + public bool Equals(LabelSequence other) + { + if (_hashCode != other._hashCode) return false; + if (Length != other.Length) return false; + + return Names.Equals(other.Names) && Values.Equals(other.Values); + } + + public override bool Equals(object? obj) + { + if (obj is LabelSequence ls) + return Equals(ls); + + return false; + } + + public override int GetHashCode() => _hashCode; + + private readonly int _hashCode; + + private int CalculateHashCode() + { + int hashCode = 0; + + unchecked + { + hashCode ^= (Names.GetHashCode() * 397); + hashCode ^= (Values.GetHashCode() * 397); + } + + return hashCode; + } + + /// + /// Converts the label sequence to a dictionary. + /// + public IDictionary ToDictionary() + { + var result = new Dictionary(); + + var nameEnumerator = Names.GetEnumerator(); + var valueEnumerator = Values.GetEnumerator(); + + for (var i = 0; i < Names.Length; i++) + { + if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); + if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); + + result.Add(nameEnumerator.Current, valueEnumerator.Current); + } + + return result; + } + + public override string ToString() + { + // Just for debugging. + return $"({Length})" + string.Join("; ", ToDictionary().Select(pair => $"{pair.Key} = {pair.Value}")); + } +} diff --git a/Prometheus/LowGranularityTimeSource.cs b/Prometheus/LowGranularityTimeSource.cs new file mode 100644 index 00000000..92989936 --- /dev/null +++ b/Prometheus/LowGranularityTimeSource.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; + +namespace Prometheus; + +/// +/// We occasionally need timestamps to attach to metrics metadata. In high-performance code, calling the standard get-timestamp functions can be a nontrivial cost. +/// This class does some caching to avoid calling the expensive timestamp functions too often, giving an accurate but slightly lower granularity clock as one might otherwise get. +/// +internal static class LowGranularityTimeSource +{ + [ThreadStatic] + private static double LastUnixSeconds; + + [ThreadStatic] + private static long LastStopwatchTimestamp; + + [ThreadStatic] + private static int LastTickCount; + + public static double GetSecondsFromUnixEpoch() + { + UpdateIfRequired(); + + return LastUnixSeconds; + } + + public static long GetStopwatchTimestamp() + { + UpdateIfRequired(); + + return LastStopwatchTimestamp; + } + + private static void UpdateIfRequired() + { + var currentTickCount = Environment.TickCount; + + if (LastTickCount != currentTickCount) + { + LastUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; + LastStopwatchTimestamp = Stopwatch.GetTimestamp(); + LastTickCount = currentTickCount; + } + } +} diff --git a/Prometheus/ManagedLifetimeCounter.cs b/Prometheus/ManagedLifetimeCounter.cs new file mode 100644 index 00000000..6e55b1c0 --- /dev/null +++ b/Prometheus/ManagedLifetimeCounter.cs @@ -0,0 +1,103 @@ +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeCounter : ManagedLifetimeMetricHandle, ICollector +{ + static ManagedLifetimeCounter() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeCounter(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public ICounter Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeCounter instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. + public ICounter WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public ICounter WithLabels(ReadOnlyMemory labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + + public ICounter WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } + #endregion + + private sealed class AutoLeasingInstance : ICounter + { + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) + { + _inner = inner; + _labelValues = labelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly ReadOnlyMemory _labelValues; + + public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + + public void Inc(double increment) => Inc(increment, null); + public void Inc(Exemplar? exemplar) => Inc(increment: 1, exemplar: exemplar); + + public void Inc(double increment, Exemplar? exemplar) + { + var args = new IncArgs(increment, exemplar); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incCoreFunc, args, _labelValues.Span); + } + + private readonly struct IncArgs(double increment, Exemplar? exemplar) + { + public readonly double Increment = increment; + public readonly Exemplar? Exemplar = exemplar; + } + + private static void IncCore(IncArgs args, ICounter counter) => counter.Inc(args.Increment, args.Exemplar); + private static readonly Action _incCoreFunc = IncCore; + + public void IncTo(double targetValue) + { + var args = new IncToArgs(targetValue); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incToCoreFunc, args, _labelValues.Span); + } + + private readonly struct IncToArgs(double targetValue) + { + public readonly double TargetValue = targetValue; + } + + private static void IncToCore(IncToArgs args, ICounter counter) => counter.IncTo(args.TargetValue); + private static readonly Action _incToCoreFunc = IncToCore; + } +} diff --git a/Prometheus/ManagedLifetimeGauge.cs b/Prometheus/ManagedLifetimeGauge.cs new file mode 100644 index 00000000..c0efd7b6 --- /dev/null +++ b/Prometheus/ManagedLifetimeGauge.cs @@ -0,0 +1,147 @@ +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeGauge : ManagedLifetimeMetricHandle, ICollector +{ + static ManagedLifetimeGauge() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeGauge(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public IGauge Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeGauge instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. + public IGauge WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public IGauge WithLabels(ReadOnlyMemory labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + + public IGauge WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } + #endregion + + private sealed class AutoLeasingInstance : IGauge + { + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) + { + _inner = inner; + _labelValues = labelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly ReadOnlyMemory _labelValues; + + public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + + public void Inc(double increment = 1) + { + var args = new IncArgs(increment); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incCoreFunc, args, _labelValues.Span); + } + + private readonly struct IncArgs(double increment) + { + public readonly double Increment = increment; + } + + private static void IncCore(IncArgs args, IGauge gauge) => gauge.Inc(args.Increment); + private static readonly Action _incCoreFunc = IncCore; + + public void Set(double val) + { + var args = new SetArgs(val); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_setCoreFunc, args, _labelValues.Span); + } + + private readonly struct SetArgs(double val) + { + public readonly double Val = val; + } + + private static void SetCore(SetArgs args, IGauge gauge) => gauge.Set(args.Val); + private static readonly Action _setCoreFunc = SetCore; + + public void Dec(double decrement = 1) + { + var args = new DecArgs(decrement); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_decCoreFunc, args, _labelValues.Span); + } + + private readonly struct DecArgs(double decrement) + { + public readonly double Decrement = decrement; + } + + private static void DecCore(DecArgs args, IGauge gauge) => gauge.Dec(args.Decrement); + private static readonly Action _decCoreFunc = DecCore; + + public void IncTo(double targetValue) + { + var args = new IncToArgs(targetValue); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incToCoreFunc, args, _labelValues.Span); + } + + private readonly struct IncToArgs(double targetValue) + { + public readonly double TargetValue = targetValue; + } + + private static void IncToCore(IncToArgs args, IGauge gauge) => gauge.IncTo(args.TargetValue); + private static readonly Action _incToCoreFunc = IncToCore; + + public void DecTo(double targetValue) + { + var args = new DecToArgs(targetValue); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_decToCoreFunc, args, _labelValues.Span); + } + + private readonly struct DecToArgs(double targetValue) + { + public readonly double TargetValue = targetValue; + } + + private static void DecToCore(DecToArgs args, IGauge gauge) => gauge.DecTo(args.TargetValue); + private static readonly Action _decToCoreFunc = DecToCore; + } +} diff --git a/Prometheus/ManagedLifetimeHistogram.cs b/Prometheus/ManagedLifetimeHistogram.cs new file mode 100644 index 00000000..6971fe46 --- /dev/null +++ b/Prometheus/ManagedLifetimeHistogram.cs @@ -0,0 +1,107 @@ +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeHistogram : ManagedLifetimeMetricHandle, ICollector +{ + static ManagedLifetimeHistogram() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeHistogram(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public IHistogram Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeHistogram instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. + public IHistogram WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public IHistogram WithLabels(ReadOnlyMemory labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + + public IHistogram WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } + #endregion + + private sealed class AutoLeasingInstance : IHistogram + { + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) + { + _inner = inner; + _labelValues = labelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly ReadOnlyMemory _labelValues; + + public double Sum => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + public long Count => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + + public void Observe(double val, long count) + { + var args = new ObserveValCountArgs(val, count); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_observeValCountCoreFunc, args, _labelValues.Span); + } + + private readonly struct ObserveValCountArgs(double val, long count) + { + public readonly double Val = val; + public readonly long Count = count; + } + + private static void ObserveValCountCore(ObserveValCountArgs args, IHistogram histogram) => histogram.Observe(args.Val, args.Count); + private static readonly Action _observeValCountCoreFunc = ObserveValCountCore; + + public void Observe(double val, Exemplar? exemplar) + { + var args = new ObserveValExemplarArgs(val, exemplar); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_observeValExemplarCoreFunc, args, _labelValues.Span); + } + + private readonly struct ObserveValExemplarArgs(double val, Exemplar? exemplar) + { + public readonly double Val = val; + public readonly Exemplar? Exemplar = exemplar; + } + + private static void ObserveValExemplarCore(ObserveValExemplarArgs args, IHistogram histogram) => histogram.Observe(args.Val, args.Exemplar); + private static readonly Action _observeValExemplarCoreFunc = ObserveValExemplarCore; + + public void Observe(double val) + { + Observe(val, null); + } + } +} diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs new file mode 100644 index 00000000..22d67178 --- /dev/null +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -0,0 +1,223 @@ +namespace Prometheus; + +internal sealed class ManagedLifetimeMetricFactory : IManagedLifetimeMetricFactory +{ + public ManagedLifetimeMetricFactory(MetricFactory inner, TimeSpan expiresAfter) + { + // .NET Framework requires the timer to fit in int.MaxValue and we will have hidden failures to expire if it does not. + // For simplicity, let's just limit it to 1 day, which should be enough for anyone. + if (expiresAfter > TimeSpan.FromDays(1)) + throw new ArgumentOutOfRangeException(nameof(expiresAfter), "Automatic metric expiration time must be no greater than 1 day."); + + _inner = inner; + _expiresAfter = expiresAfter; + } + + private readonly MetricFactory _inner; + private readonly TimeSpan _expiresAfter; + + public IManagedLifetimeMetricFactory WithLabels(IDictionary labels) + { + return new LabelEnrichingManagedLifetimeMetricFactory(this, labels); + } + + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[]? instanceLabelNames, CounterConfiguration? configuration) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); + + _countersLock.EnterReadLock(); + + try + { + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_counters.TryGetValue(identity, out var existing)) + return existing; + } + finally + { + _countersLock.ExitReadLock(); + } + + var metric = _inner.CreateCounter(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeCounter(metric, _expiresAfter); + + _countersLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_counters.TryAdd(identity, instance)) + return instance; + + return _counters[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_counters.TryGetValue(identity, out var existing)) + return existing; + + _counters.Add(identity, instance); + return instance; +#endif + } + finally + { + _countersLock.ExitWriteLock(); + } + } + + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); + + _gaugesLock.EnterReadLock(); + + try + { + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_gauges.TryGetValue(identity, out var existing)) + return existing; + } + finally + { + _gaugesLock.ExitReadLock(); + } + + var metric = _inner.CreateGauge(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeGauge(metric, _expiresAfter); + + _gaugesLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_gauges.TryAdd(identity, instance)) + return instance; + + return _gauges[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_gauges.TryGetValue(identity, out var existing)) + return existing; + + _gauges.Add(identity, instance); + return instance; +#endif + } + finally + { + _gaugesLock.ExitWriteLock(); + } + } + + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); + + _histogramsLock.EnterReadLock(); + + try + { + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_histograms.TryGetValue(identity, out var existing)) + return existing; + } + finally + { + _histogramsLock.ExitReadLock(); + } + + var metric = _inner.CreateHistogram(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeHistogram(metric, _expiresAfter); + + _histogramsLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_histograms.TryAdd(identity, instance)) + return instance; + + return _histograms[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_histograms.TryGetValue(identity, out var existing)) + return existing; + + _histograms.Add(identity, instance); + return instance; +#endif + } + finally + { + _histogramsLock.ExitWriteLock(); + } + } + + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); + + _summariesLock.EnterReadLock(); + + try + { + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_summaries.TryGetValue(identity, out var existing)) + return existing; + } + finally + { + _summariesLock.ExitReadLock(); + } + + var metric = _inner.CreateSummary(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeSummary(metric, _expiresAfter); + + _summariesLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_summaries.TryAdd(identity, instance)) + return instance; + + return _summaries[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_summaries.TryGetValue(identity, out var existing)) + return existing; + + _summaries.Add(identity, instance); + return instance; +#endif + } + finally + { + _summariesLock.ExitWriteLock(); + } + } + + /// + /// Gets all the existing label names predefined either in the factory or in the registry. + /// + internal StringSequence GetAllStaticLabelNames() => _inner.GetAllStaticLabelNames(); + + // We need to reuse existing instances of lifetime-managed metrics because the user might not want to cache it. + // This somewhat duplicates the metric identity tracking logic in CollectorRegistry but this is intentional, as we really do need to do this work on two layers. + // We never remove collectors from here as long as the factory is alive. The expectation is that there is not an unbounded set of label names, so this set is non-gigantic. + private readonly Dictionary _counters = new(); + private readonly ReaderWriterLockSlim _countersLock = new(); + + private readonly Dictionary _gauges = new(); + private readonly ReaderWriterLockSlim _gaugesLock = new(); + + private readonly Dictionary _histograms = new(); + private readonly ReaderWriterLockSlim _histogramsLock = new(); + + private readonly Dictionary _summaries = new(); + private readonly ReaderWriterLockSlim _summariesLock = new(); +} diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs new file mode 100644 index 00000000..3045338a --- /dev/null +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -0,0 +1,497 @@ +using System.Buffers; + +namespace Prometheus; + +/// +/// Represents a metric whose lifetime is managed by the caller, either via explicit leases or via extend-on-use behavior (implicit leases). +/// +/// +/// Each metric handle maintains a reaper task that occasionally removes metrics that have expired. The reaper is started +/// when the first lifetime-managed metric is created and terminates when the last lifetime-managed metric expires. +/// This does mean that the metric handle may keep objects alive until expiration, even if the handle itself is no longer used. +/// +internal abstract class ManagedLifetimeMetricHandle + : IManagedLifetimeMetricHandle, INotifyLeaseEnded + where TChild : ChildBase, TMetricInterface + where TMetricInterface : ICollectorChild +{ + internal ManagedLifetimeMetricHandle(Collector metric, TimeSpan expiresAfter) + { + _reaperFunc = Reaper; + + _metric = metric; + _expiresAfter = expiresAfter; + } + + protected readonly Collector _metric; + protected readonly TimeSpan _expiresAfter; + + #region Lease(string[]) + public IDisposable AcquireLease(out TMetricInterface metric, params string[] labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeLease(child); + } + + public RefLease AcquireRefLease(out TMetricInterface metric, params string[] labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeRefLease(child); + } + + public void WithLease(Action action, params string[] labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(child); + } + + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(arg, child); + } + + public async Task WithLeaseAsync(Func action, params string[] labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + await action(metric); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return func(metric); + } + + public async Task WithLeaseAsync(Func> func, params string[] labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return await func(metric); + } + #endregion + + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out TMetricInterface metric, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeLease(child); + } + + public RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeRefLease(child); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(child); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(arg, child); + } + + public async Task WithLeaseAsync(Func action, ReadOnlyMemory labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + await action(metric); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return func(metric); + } + + public async Task WithLeaseAsync(Func> func, ReadOnlyMemory labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return await func(metric); + } + #endregion + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out TMetricInterface metric, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeLease(child); + } + + public RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeRefLease(child); + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(child); + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(arg, child); + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return func(metric); + } + #endregion + + public abstract ICollector WithExtendLifetimeOnUse(); + + /// + /// Internal to allow the delay logic to be replaced in test code, enabling (non-)expiration on demand. + /// + internal IDelayer Delayer = RealDelayer.Instance; + + #region Lease tracking + private readonly Dictionary _lifetimes = new(); + + // Guards the collection but not the contents. + private readonly ReaderWriterLockSlim _lifetimesLock = new(); + + private bool HasAnyTrackedLifetimes() + { + _lifetimesLock.EnterReadLock(); + + try + { + return _lifetimes.Count != 0; + } + finally + { + _lifetimesLock.ExitReadLock(); + } + } + + /// + /// For testing only. Sets all keepalive timestamps to a time in the disstant past, + /// which will cause all lifetimes to expire (if they have no leases). + /// + internal void SetAllKeepaliveTimestampsToDistantPast() + { + // We cannot just zero this because zero is the machine start timestamp, so zero is not necessarily + // far in the past (especially if the machine is a build agent that just started up). 1 year negative should work, though. + var distantPast = -PlatformCompatibilityHelpers.ElapsedToTimeStopwatchTicks(TimeSpan.FromDays(365)); + + _lifetimesLock.EnterReadLock(); + + try + { + foreach (var lifetime in _lifetimes.Values) + Volatile.Write(ref lifetime.KeepaliveTimestamp, distantPast); + } + finally + { + _lifetimesLock.ExitReadLock(); + } + } + + /// + /// For anomaly analysis during testing only. + /// + internal void DebugDumpLifetimes() + { + _lifetimesLock.EnterReadLock(); + + try + { + Console.WriteLine($"Dumping {_lifetimes.Count} lifetimes of {_metric}. Reaper status: {Volatile.Read(ref _reaperActiveBool)}."); + + foreach (var pair in _lifetimes) + { + Console.WriteLine($"{pair.Key} -> {pair.Value}"); + } + } + finally + { + _lifetimesLock.ExitReadLock(); + } + } + + private IDisposable TakeLease(TChild child) + { + var lifetime = GetOrCreateLifetimeAndIncrementLeaseCount(child); + EnsureReaperActive(); + + return new Lease(this, child, lifetime); + } + + private RefLease TakeRefLease(TChild child) + { + var lifetime = GetOrCreateLifetimeAndIncrementLeaseCount(child); + EnsureReaperActive(); + + return new RefLease(this, child, lifetime); + } + + private ChildLifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child) + { + _lifetimesLock.EnterReadLock(); + + try + { + // Ideally, there already exists a registered lifetime for this metric instance. + if (_lifetimes.TryGetValue(child, out var existing)) + { + // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. + Interlocked.Increment(ref existing.LeaseCount); + return existing; + } + } + finally + { + _lifetimesLock.ExitReadLock(); + } + + // No lifetime registered yet - we need to take a write lock and register it. + var newLifetime = new ChildLifetimeInfo + { + LeaseCount = 1 + }; + + _lifetimesLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_lifetimes.TryAdd(child, newLifetime)) + return newLifetime; + + var existing = _lifetimes[child]; + + // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. + // Even if something does, it is not the end of the world - the reaper will create a new lifetime when it realizes this happened. + Interlocked.Increment(ref existing.LeaseCount); + return existing; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_lifetimes.TryGetValue(child, out var existing)) + { + // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. + // Even if something does, it is not the end of the world - the reaper will create a new lifetime when it realizes this happened. + Interlocked.Increment(ref existing.LeaseCount); + return existing; + } + + _lifetimes.Add(child, newLifetime); + return newLifetime; +#endif + } + finally + { + _lifetimesLock.ExitWriteLock(); + } + } + + internal void OnLeaseEnded(TChild child, ChildLifetimeInfo lifetime) + { + // Update keepalive timestamp before anything else, to avoid racing. + Volatile.Write(ref lifetime.KeepaliveTimestamp, LowGranularityTimeSource.GetStopwatchTimestamp()); + + // If the lifetime has been ended while we still held a lease, it means there was a race that we lost. + // The metric instance may or may not be still alive. To ensure proper cleanup, we re-register a lifetime + // for the metric instance, which will ensure it gets cleaned up when it expires. + if (Volatile.Read(ref lifetime.Ended)) + { + // We just take a new lease and immediately dispose it. We are guaranteed not to loop here because the + // reaper removes lifetimes from the dictionary once ended, so we can never run into the same lifetime again. + TakeRefLease(child).Dispose(); + } + + // Finally, decrement the lease count to relinquish any claim on extending the lifetime. + Interlocked.Decrement(ref lifetime.LeaseCount); + } + + void INotifyLeaseEnded.OnLeaseEnded(object child, ChildLifetimeInfo lifetime) + { + OnLeaseEnded((TChild)child, lifetime); + } + + private sealed class Lease(ManagedLifetimeMetricHandle parent, TChild child, ChildLifetimeInfo lifetime) : IDisposable + { + public void Dispose() => parent.OnLeaseEnded(child, lifetime); + } +#endregion + + #region Reaper + // Whether the reaper is currently active. This is set to true when a metric instance is created and + // reset when the last metric instance expires (after which it may be set again). + // We use atomic operations without locking. + private int _reaperActiveBool = ReaperInactive; + + private const int ReaperActive = 1; + private const int ReaperInactive = 0; + + /// + /// Call this immediately after creating a metric instance that will eventually expire. + /// + private void EnsureReaperActive() + { + if (Interlocked.CompareExchange(ref _reaperActiveBool, ReaperActive, ReaperInactive) == ReaperActive) + { + // It was already active - nothing for us to do. + return; + } + + _ = Task.Run(_reaperFunc); + } + + private async Task Reaper() + { + while (true) + { + var now = LowGranularityTimeSource.GetStopwatchTimestamp(); + + // Will contains the results of pass 1. + TChild[] expiredInstancesBuffer = null!; + int expiredInstanceCount = 0; + + // Pass 1: holding only a read lock, make a list of metric instances that have expired. + _lifetimesLock.EnterReadLock(); + + try + { + try + { + expiredInstancesBuffer = ArrayPool.Shared.Rent(_lifetimes.Count); + + foreach (var pair in _lifetimes) + { + if (Volatile.Read(ref pair.Value.LeaseCount) != 0) + continue; // Not expired. + + if (PlatformCompatibilityHelpers.StopwatchGetElapsedTime(Volatile.Read(ref pair.Value.KeepaliveTimestamp), now) < _expiresAfter) + continue; // Not expired. + + // No leases and keepalive has expired - it is an expired instance! + expiredInstancesBuffer[expiredInstanceCount++] = pair.Key; + } + } + finally + { + _lifetimesLock.ExitReadLock(); + } + + // Pass 2: if we have any work to do, take a write lock and remove the expired metric instances, + // assuming our judgement about their expiration remains valid. We process and lock one by one, + // to avoid holding locks for a long duration if many items expire at once - we are not in any rush. + for (var i = 0; i < expiredInstanceCount; i++) + { + var expiredInstance = expiredInstancesBuffer[i]; + + _lifetimesLock.EnterWriteLock(); + + try + { + if (!_lifetimes.TryGetValue(expiredInstance, out var lifetime)) + continue; // Already gone, nothing for us to do. + + // We need to check again whether the metric instance is still expired, because it may have been + // renewed by a new lease in the meantime. If it is still expired, we can remove it. + if (Volatile.Read(ref lifetime.LeaseCount) != 0) + continue; // Not expired. + + if (PlatformCompatibilityHelpers.StopwatchGetElapsedTime(Volatile.Read(ref lifetime.KeepaliveTimestamp), now) < _expiresAfter) + continue; // Not expired. + + // No leases and keepalive has expired - it is an expired instance! + + // We mark the old lifetime as ended - if it happened that it got associated with a new lease + // (which is possible because we do not prevent lease-taking while in this loop), the new lease + // upon being ended will re-register the lifetime instead of just extending the existing one. + // We can be certain that any concurrent lifetime-affecting logic is using the same LifetimeInfo + // instance because the lifetime dictionary remains locked until we are done (by which time this flag is set). + Volatile.Write(ref lifetime.Ended, true); + + _lifetimes.Remove(expiredInstance); + + // If we did encounter a race, removing the metric instance here means that some metric value updates + // may go missing (until the next lease creates a new instance). This is acceptable behavior, to keep the code simple. + expiredInstance.Remove(); + } + finally + { + _lifetimesLock.ExitWriteLock(); + } + } + } + finally + { + ArrayPool.Shared.Return(expiredInstancesBuffer); + } + + // Check if we need to shut down the reaper or keep going. + _lifetimesLock.EnterReadLock(); + + try + { + if (_lifetimes.Count != 0) + goto has_more_work; + } + finally + { + _lifetimesLock.ExitReadLock(); + } + + CleanupReaper(); + return; + + has_more_work: + // Work done! Go sleep a bit and come back when something may have expired. + // We do not need to be too aggressive here, as expiration is not a hard schedule guarantee. + await Delayer.Delay(_expiresAfter); + } + } + + /// + /// Called when the reaper has noticed that all metric instances have expired and it has no more work to do. + /// + private void CleanupReaper() + { + Volatile.Write(ref _reaperActiveBool, ReaperInactive); + + // The reaper is now gone. However, as we do not use locking here it is possible that someone already + // added metric instances (which saw "oh reaper is still running") before we got here. Let's check - if + // there appear to be metric instances registered, we may need to start the reaper again. + if (HasAnyTrackedLifetimes()) + EnsureReaperActive(); + } + + private readonly Func _reaperFunc; + #endregion +} \ No newline at end of file diff --git a/Prometheus/ManagedLifetimeMetricIdentity.cs b/Prometheus/ManagedLifetimeMetricIdentity.cs new file mode 100644 index 00000000..2b4babab --- /dev/null +++ b/Prometheus/ManagedLifetimeMetricIdentity.cs @@ -0,0 +1,66 @@ +namespace Prometheus; + +/// +/// For managed lifetime metrics, we just want to uniquely identify metric instances so we can cache them. +/// We differentiate by the family name + the set of unique instance label names applied to the instance. +/// +/// Managed lifetime metrics are not differentiated by static labels because the static labels are applied +/// in a lower layer (the underlying MetricFactory) and cannot differ within a single ManagedLifetimeMetricFactory. +/// +internal readonly struct ManagedLifetimeMetricIdentity : IEquatable +{ + public readonly string MetricFamilyName; + public readonly StringSequence InstanceLabelNames; + + private readonly int _hashCode; + + public ManagedLifetimeMetricIdentity(string metricFamilyName, StringSequence instanceLabelNames) + { + MetricFamilyName = metricFamilyName; + InstanceLabelNames = instanceLabelNames; + + _hashCode = CalculateHashCode(metricFamilyName, instanceLabelNames); + } + + public bool Equals(ManagedLifetimeMetricIdentity other) + { + if (_hashCode != other._hashCode) + return false; + + if (!string.Equals(MetricFamilyName, other.MetricFamilyName, StringComparison.Ordinal)) + return false; + + if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) + return false; + + return true; + } + + public override int GetHashCode() + { + return _hashCode; + } + + private static int CalculateHashCode(string metricFamilyName, StringSequence instanceLabelNames) + { + unchecked + { + int hashCode = 0; + + hashCode ^= metricFamilyName.GetHashCode() * 997; + hashCode ^= instanceLabelNames.GetHashCode() * 397; + + return hashCode; + } + } + + public override string ToString() + { + return $"{MetricFamilyName}{InstanceLabelNames}"; + } + + public override bool Equals(object? obj) + { + return obj is ManagedLifetimeMetricIdentity identity && Equals(identity); + } +} diff --git a/Prometheus/ManagedLifetimeSummary.cs b/Prometheus/ManagedLifetimeSummary.cs new file mode 100644 index 00000000..5cb1f8d0 --- /dev/null +++ b/Prometheus/ManagedLifetimeSummary.cs @@ -0,0 +1,81 @@ +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeSummary : ManagedLifetimeMetricHandle, ICollector +{ + static ManagedLifetimeSummary() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeSummary(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public ISummary Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeSummary instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. + public ISummary WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public ISummary WithLabels(ReadOnlyMemory labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + + public ISummary WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } + #endregion + + private sealed class AutoLeasingInstance : ISummary + { + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) + { + _inner = inner; + _labelValues = labelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly ReadOnlyMemory _labelValues; + + public void Observe(double val) + { + var args = new ObserveArgs(val); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_observeCoreFunc, args, _labelValues.Span); + } + + private readonly struct ObserveArgs(double val) + { + public readonly double Val = val; + } + + private static void ObserveCore(ObserveArgs args, ISummary summary) => summary.Observe(args.Val); + private static readonly Action _observeCoreFunc = ObserveCore; + } +} diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs new file mode 100644 index 00000000..ed41a76f --- /dev/null +++ b/Prometheus/MeterAdapter.cs @@ -0,0 +1,516 @@ +#if NET6_0_OR_GREATER +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Prometheus; + +/// +/// Publishes .NET Meters as Prometheus metrics. +/// +public sealed class MeterAdapter : IDisposable +{ + public static IDisposable StartListening() => StartListening(MeterAdapterOptions.Default); + + public static IDisposable StartListening(MeterAdapterOptions options) + { + // If we are re-registering an adapter with the default options, just pretend and move on. + // The purpose of this code is to avoid double-counting metrics if the adapter is registered twice with the default options. + // This could happen because in 7.0.0 we added automatic registration of the adapters on startup, but the user might still + // have a manual registration active from 6.0.0 days. We do this small thing here to make upgrading less hassle. + if (options == MeterAdapterOptions.Default) + { + if (options.Registry.PreventMeterAdapterRegistrationWithDefaultOptions) + return new NoopDisposable(); + + options.Registry.PreventMeterAdapterRegistrationWithDefaultOptions = true; + } + + return new MeterAdapter(options); + } + + private MeterAdapter(MeterAdapterOptions options) + { + _createGaugeFunc = CreateGauge; + _createHistogramFunc = CreateHistogram; + + _options = options; + + _registry = options.Registry; + + var baseFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(_options.Registry); + _factory = (ManagedLifetimeMetricFactory)baseFactory.WithManagedLifetime(expiresAfter: options.MetricsExpireAfter); + + _inheritedStaticLabelNames = _factory.GetAllStaticLabelNames().ToArray(); + + _listener.InstrumentPublished = OnInstrumentPublished; + _listener.MeasurementsCompleted += OnMeasurementsCompleted; + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + _listener.SetMeasurementEventCallback(OnMeasurementRecorded); + + var regularFactory = Metrics.WithCustomRegistry(_registry); + _instrumentsConnected = regularFactory.CreateGauge("prometheus_net_meteradapter_instruments_connected", "Number of instruments that are currently connected to the adapter."); + + _listener.Start(); + + _registry.AddBeforeCollectCallback(delegate + { + // ICollectorRegistry does not support unregistering the callback, so we just no-op when disposed. + // The expected pattern is that any disposal of the pipeline also throws away the ICollectorRegistry. + lock (_disposedLock) + if (_disposed) + return; + + // Seems OK to call even when _listener has been disposed. + _listener.RecordObservableInstruments(); + }); + } + + private readonly MeterAdapterOptions _options; + + private readonly CollectorRegistry _registry; + private readonly ManagedLifetimeMetricFactory _factory; + private readonly string[] _inheritedStaticLabelNames; + + private readonly Gauge _instrumentsConnected; + + private readonly MeterListener _listener = new MeterListener(); + + private bool _disposed; + private readonly object _disposedLock = new(); + + public void Dispose() + { + lock (_disposedLock) + { + if (_disposed) + return; + + _disposed = true; + } + + _listener.Dispose(); + } + + private void OnInstrumentPublished(Instrument instrument, MeterListener listener) + { + if (!_options.InstrumentFilterPredicate(instrument)) + return; // This instrument is not wanted. + + _instrumentsConnected.Inc(); + + _instrumentPrometheusNames.TryAdd(instrument, TranslateInstrumentNameToPrometheusName(instrument)); + _instrumentPrometheusHelp.TryAdd(instrument, TranslateInstrumentDescriptionToPrometheusHelp(instrument)); + + try + { + // Always listen to everything - we want to adapt all input metrics to Prometheus metrics. + listener.EnableMeasurementEvents(instrument); + } + catch (Exception ex) + { + // Eat exceptions here to ensure no harm comes of failed enabling. + // The previous generation EventCounter infrastructure has proven quite buggy and while Meters may not be afflicted with the same problems, let's be paranoid. + Trace.WriteLine($"Failed to enable Meter listening for {instrument.Name}: {ex.Message}"); + } + } + + private void OnMeasurementRecorded( + Instrument instrument, + TMeasurement measurement, + ReadOnlySpan> tags, + object? state) + where TMeasurement : struct + { + // NOTE: If we throw an exception from this, it can lead to the instrument becoming inoperable (no longer measured). Let's not do that. + + // We assemble and sort the label values in a temporary buffer. If the metric instance is already known + // to prometheus-net, this means no further memory allocation for the label values is required below. + var labelValuesBuffer = ArrayPool.Shared.Rent(tags.Length); + + try + { + double value = unchecked(measurement switch + { + byte x => x, + short x => x, + int x => x, + long x => x, + float x => (double)x, + double x => x, + decimal x => (double)x, + _ => throw new NotSupportedException($"Measurement type {typeof(TMeasurement).Name} is not supported.") + }); + + // We do not represent any of the "counter" style .NET meter types as counters because + // they may be re-created on the .NET Meters side at any time, decrementing the value! + + if (instrument is Counter) + { + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); + + // A measurement is the increment. + context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); + } + else if (instrument is ObservableCounter) + { + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); + + // A measurement is the current value. We transform it into a Set() to allow the counter to reset itself (unusual but who are we to say no). + context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); + } +#if NET7_0_OR_GREATER + else if (instrument is UpDownCounter) + { + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); + + // A measurement is the increment. + context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); + } +#endif + else if (instrument is ObservableGauge +#if NET7_0_OR_GREATER + or ObservableUpDownCounter +#endif + ) + { + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); + + // A measurement is the current value. + context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); + } + else if (instrument is Histogram) + { + var context = GetOrCreateHistogramContext(instrument, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); + + // A measurement is the observed value. + context.MetricInstanceHandle.WithLease(_observeHistogramFunc, value, labelValues); + } + else + { + Trace.WriteLine($"Instrument {instrument.Name} is of an unsupported type: {instrument.GetType().Name}."); + } + } + catch (Exception ex) + { + Trace.WriteLine($"{instrument.Name} collection failed: {ex.Message}"); + } + finally + { + ArrayPool.Shared.Return(labelValuesBuffer); + } + } + + private static void IncrementGauge(double value, IGauge gauge) => gauge.Inc(value); + private static readonly Action _incrementGaugeFunc = IncrementGauge; + + private static void SetGauge(double value, IGauge gauge) => gauge.Set(value); + private static readonly Action _setGaugeFunc = SetGauge; + + private static void ObserveHistogram(double value, IHistogram histogram) => histogram.Observe(value); + private static readonly Action _observeHistogramFunc = ObserveHistogram; + + // Cache key: Instrument + user-ordered list of label names. + // NB! The same Instrument may be cached multiple times, with the same label names in a different order! + private readonly struct CacheKey(Instrument instrument, StringSequence meterLabelNames) : IEquatable + { + public Instrument Instrument { get; } = instrument; + + // Order is whatever was provided by the caller of the .NET Meters API. + public StringSequence MeterLabelNames { get; } = meterLabelNames; + + public override readonly bool Equals(object? obj) => obj is CacheKey other && Equals(other); + + public override readonly int GetHashCode() => _hashCode; + private readonly int _hashCode = HashCode.Combine(instrument, meterLabelNames); + + public readonly bool Equals(CacheKey other) => Instrument == other.Instrument && MeterLabelNames.Equals(other.MeterLabelNames); + } + + // Cache value: Prometheus metric handle + Prometheus-ordered indexes into original Meters tags list. + // Not all Meter tags may be preserved, as some may have conflicted with static labels and been filtered out. + private sealed class MetricContext( + IManagedLifetimeMetricHandle metricInstanceHandle, + int[] prometheusLabelValueIndexes) + where TMetricInterface : ICollectorChild + { + public IManagedLifetimeMetricHandle MetricInstanceHandle { get; } = metricInstanceHandle; + + // Index into the .NET Meters API labels list, indicating which original label to take the value from. + public int[] PrometheusLabelValueIndexes { get; } = prometheusLabelValueIndexes; + } + + private readonly Dictionary> _gaugeCache = new(); + private readonly ReaderWriterLockSlim _gaugeCacheLock = new(); + + private readonly Dictionary> _histogramCache = new(); + private readonly ReaderWriterLockSlim _histogramCacheLock = new(); + + private MetricContext GetOrCreateGaugeContext(Instrument instrument, in ReadOnlySpan> tags) + => GetOrCreateMetricContext(instrument, tags, _createGaugeFunc, _gaugeCacheLock, _gaugeCache); + + private MetricContext GetOrCreateHistogramContext(Instrument instrument, in ReadOnlySpan> tags) + => GetOrCreateMetricContext(instrument, tags, _createHistogramFunc, _histogramCacheLock, _histogramCache); + + private IManagedLifetimeMetricHandle CreateGauge(Instrument instrument, string name, string help, string[] labelNames) + => _factory.CreateGauge(name, help, labelNames, null); + private readonly Func> _createGaugeFunc; + + private IManagedLifetimeMetricHandle CreateHistogram(Instrument instrument, string name, string help, string[] labelNames) + => _factory.CreateHistogram(name, help, labelNames, new HistogramConfiguration + { + // We outsource the bucket definition to the callback in options, as it might need to be different for different instruments. + Buckets = _options.ResolveHistogramBuckets(instrument) + }); + private readonly Func> _createHistogramFunc; + + private MetricContext GetOrCreateMetricContext( + Instrument instrument, + in ReadOnlySpan> tags, + Func> metricFactory, + ReaderWriterLockSlim cacheLock, + Dictionary> cache) + where TMetricInstance : ICollectorChild + { + // Use a pooled array for the cache key if we are performing a lookup. + // This avoids allocating a new array if the context is already cached. + var meterLabelNamesBuffer = ArrayPool.Shared.Rent(tags.Length); + var meterLabelNamesCount = tags.Length; + + try + { + for (var i = 0; i < tags.Length; i++) + meterLabelNamesBuffer[i] = tags[i].Key; + + var meterLabelNames = StringSequence.From(meterLabelNamesBuffer.AsMemory(0, meterLabelNamesCount)); + var cacheKey = new CacheKey(instrument, meterLabelNames); + + cacheLock.EnterReadLock(); + + try + { + // In the common case, we will find the context in the cache and can return it here without any memory allocation. + if (cache.TryGetValue(cacheKey, out var context)) + return context; + } + finally + { + cacheLock.ExitReadLock(); + } + } + finally + { + ArrayPool.Shared.Return(meterLabelNamesBuffer); + } + + // If we got here, we did not find the context in the cache. Make a new one. + return CreateMetricContext(instrument, tags, metricFactory, cacheLock, cache); + } + + private MetricContext CreateMetricContext( + Instrument instrument, + in ReadOnlySpan> tags, + Func> metricFactory, + ReaderWriterLockSlim cacheLock, + Dictionary> cache) + where TMetricInstance : ICollectorChild + { + var meterLabelNamesBuffer = new string[tags.Length]; + + for (var i = 0; i < tags.Length; i++) + meterLabelNamesBuffer[i] = tags[i].Key; + + var meterLabelNames = StringSequence.From(meterLabelNamesBuffer); + var cacheKey = new CacheKey(instrument, meterLabelNames); + + // Create the context before taking any locks, to avoid holding the cache too long. + DeterminePrometheusLabels(tags, out var prometheusLabelNames, out var prometheusLabelValueIndexes); + var metricHandle = metricFactory(instrument, _instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], prometheusLabelNames); + var newContext = new MetricContext(metricHandle, prometheusLabelValueIndexes); + + cacheLock.EnterWriteLock(); + + try + { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (cache.TryAdd(cacheKey, newContext)) + return newContext; + + return cache[cacheKey]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (cache.TryGetValue(cacheKey, out var context)) + return context; + + cache.Add(cacheKey, newContext); + return newContext; +#endif + } + finally + { + cacheLock.ExitWriteLock(); + } + } + + private void DeterminePrometheusLabels( + in ReadOnlySpan> tags, + out string[] prometheusLabelNames, + out int[] prometheusLabelValueIndexes) + { + var originalsCount = tags.Length; + + // Prometheus name of the label. + var namesBuffer = ArrayPool.Shared.Rent(originalsCount); + // Index into the original label list. + var indexesBuffer = ArrayPool.Shared.Rent(originalsCount); + // Whether the label should be skipped entirely (because it conflicts with a static label). + var skipFlagsBuffer = ArrayPool.Shared.Rent(originalsCount); + + try + { + for (var i = 0; i < tags.Length; i++) + { + var prometheusName = _tagPrometheusNames.GetOrAdd(tags[i].Key, _translateTagNameToPrometheusNameFunc); + + namesBuffer[i] = prometheusName; + indexesBuffer[i] = i; + } + + // The order of labels matters in the prometheus-net API. However, in .NET Meters the tags are unordered. + // Therefore, we need to sort the labels to ensure that we always create metrics with the same order. + Array.Sort(keys: namesBuffer, items: indexesBuffer, index: 0, length: originalsCount, StringComparer.Ordinal); + + // NOTE: As we accept random input from external code here, there is no guarantee that the labels in this code + // do not conflict with existing static labels. We must therefore take explicit action here to prevent conflict + // (as prometheus-net will detect and fault on such conflicts). We do this by inspecting the internals of the + // factory to identify conflicts with any static labels, and remove those lables from the Meters API data point + // (static overrides dynamic) if there is a match (by just skipping them in our output index set). + var preservedLabelCount = 0; + + for (var i = 0; i < tags.Length; i++) + { + skipFlagsBuffer[i] = _inheritedStaticLabelNames.Contains(namesBuffer[i], StringComparer.Ordinal); + + if (skipFlagsBuffer[i] == false) + preservedLabelCount++; + } + + prometheusLabelNames = new string[preservedLabelCount]; + prometheusLabelValueIndexes = new int[preservedLabelCount]; + + var nextIndex = 0; + + for (var i = 0; i < tags.Length; i++) + { + if (skipFlagsBuffer[i]) + continue; + + prometheusLabelNames[nextIndex] = namesBuffer[i]; + prometheusLabelValueIndexes[nextIndex] = indexesBuffer[i]; + nextIndex++; + } + } + finally + { + ArrayPool.Shared.Return(skipFlagsBuffer); + ArrayPool.Shared.Return(indexesBuffer); + ArrayPool.Shared.Return(namesBuffer); + } + } + + private void OnMeasurementsCompleted(Instrument instrument, object? state) + { + // Called when no more data is coming for an instrument. We do not do anything with the already published metrics because: + // 1) We operate on a pull model - just because the instrument goes away does not mean that the latest data from it has been pulled. + // 2) We already have a perfectly satisfactory expiration based lifetime control model, no need to complicate with a second logic alongside. + // 3) There is no 1:1 mapping between instrument and metric due to allowing flexible label name combinations, which may cause undesirable complexity. + + // We also cannot clear our mapping collections yet because it is possible that some measurement observations are still in progress! + // In other words, this may be called before the last OnMeasurementRecorded() call for the instrument has completed (perhaps even started?). + // The entire adapter data set will be collected when the Prometheus registry itself is garbage collected. + } + + private static ReadOnlySpan CopyTagValuesToLabelValues( + int[] prometheusLabelValueIndexes, + ReadOnlySpan> tags, + Span labelValues) + { + for (var i = 0; i < prometheusLabelValueIndexes.Length; i++) + { + var index = prometheusLabelValueIndexes[i]; + labelValues[i] = tags[index].Value?.ToString() ?? ""; + } + + return labelValues[..prometheusLabelValueIndexes.Length]; + } + + // We use these dictionaries to register Prometheus metrics on-demand for different instruments. + private static readonly ConcurrentDictionary _instrumentPrometheusNames = new(); + private static readonly ConcurrentDictionary _instrumentPrometheusHelp = new(); + + // We use this dictionary to translate tag names on-demand. + // Immortal set, we assume we do not get an infinite mix of tag names. + private static readonly ConcurrentDictionary _tagPrometheusNames = new(); + + private static string TranslateInstrumentNameToPrometheusName(Instrument instrument) + { + // Example input: meter "Foo.Bar.Baz" with instrument "walla-walla" + // Example output: foo_bar_baz_walla_walla + + return PrometheusNameHelpers.TranslateNameToPrometheusName($"{instrument.Meter.Name}_{instrument.Name}"); + } + + private static string TranslateTagNameToPrometheusName(string tagName) + { + // Example input: hello-there + // Example output: hello_there + + return PrometheusNameHelpers.TranslateNameToPrometheusName(tagName); + } + + private static readonly Func _translateTagNameToPrometheusNameFunc = TranslateTagNameToPrometheusName; + + [ThreadStatic] + private static StringBuilder? _prometheusHelpBuilder; + + // If the string builder grows over this, we throw it away and use a new one next time to avoid keeping a large buffer around. + private const int PrometheusHelpBuilderReusableCapacity = 1 * 1024; + + private static string TranslateInstrumentDescriptionToPrometheusHelp(Instrument instrument) + { + _prometheusHelpBuilder ??= new(PrometheusHelpBuilderReusableCapacity); + + if (!string.IsNullOrWhiteSpace(instrument.Unit)) + _prometheusHelpBuilder.Append($"({instrument.Unit}) "); + + _prometheusHelpBuilder.Append(instrument.Description); + + // Append the base type name, so we see what type of metric it is. + _prometheusHelpBuilder.Append($" ({instrument.GetType().Name})"); + + var result = _prometheusHelpBuilder.ToString(); + + // If it grew too big, throw it away. + if (_prometheusHelpBuilder.Capacity > PrometheusHelpBuilderReusableCapacity) + _prometheusHelpBuilder = null; + else + _prometheusHelpBuilder.Clear(); + + return result; + } +} +#endif diff --git a/Prometheus/MeterAdapterOptions.cs b/Prometheus/MeterAdapterOptions.cs new file mode 100644 index 00000000..b66e7e1a --- /dev/null +++ b/Prometheus/MeterAdapterOptions.cs @@ -0,0 +1,40 @@ +#if NET6_0_OR_GREATER +using System.Diagnostics.Metrics; + +namespace Prometheus; + +public sealed record MeterAdapterOptions +{ + public static MeterAdapterOptions Default => new(); + + // This is unlikely to be suitable for all cases, so you will want to customize it per-instrument. + public static readonly double[] DefaultHistogramBuckets = Histogram.ExponentialBuckets(0.01, 2, 25); + + /// + /// By default we publish all instruments from all meters but this allows you to filter by instrument. + /// + public Func InstrumentFilterPredicate { get; set; } = _ => true; + + /// + /// The .NET Meters API does not tell us (or even know itself) when a metric with a certain label combination is no longer going to receive new data. + /// To avoid building an ever-increasing store of in-memory metrics states, we delete metrics once they have not been updated in a while. + /// The idea being that metrics are useful when they are changing regularly - if a value stays the same for N minutes, it probably is not a valuable data point anymore. + /// + public TimeSpan MetricsExpireAfter { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Metrics will be published in this registry. + /// + public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; + + /// + /// If set, the value in Registry is ignored and this factory is instead used to create all the metrics. + /// + public IMetricFactory? MetricFactory { get; set; } = Metrics.DefaultFactory; + + /// + /// Enables you to define custom buckets for histogram-typed metrics. + /// + public Func ResolveHistogramBuckets { get; set; } = _ => DefaultHistogramBuckets; +} +#endif \ No newline at end of file diff --git a/Prometheus/MetricConfiguration.cs b/Prometheus/MetricConfiguration.cs new file mode 100644 index 00000000..07cc7fbc --- /dev/null +++ b/Prometheus/MetricConfiguration.cs @@ -0,0 +1,27 @@ +namespace Prometheus; + +/// +/// This class packages the options for creating metrics into a single class (with subclasses per metric type) +/// for easy extensibility of the API without adding numerous method overloads whenever new options are added. +/// +public abstract class MetricConfiguration +{ + /// + /// NOTE: Only used by APIs that do not take an explicit labelNames value as input. + /// + /// Names of all the label fields that are defined for each instance of the metric. + /// If null, the metric will be created without any instance-specific labels. + /// + /// Before using a metric that uses instance-specific labels, .WithLabels() must be called to provide values for the labels. + /// + public string[]? LabelNames { get; set; } + + /// + /// If true, the metric will not be published until its value is first modified (regardless of the specific value). + /// This is useful to delay publishing gauges that get their initial values delay-loaded. + /// + /// By default, metrics are published as soon as possible - if they do not use labels then they are published on + /// creation and if they use labels then as soon as the label values are assigned. + /// + public bool SuppressInitialValue { get; set; } +} diff --git a/Prometheus/MetricFactory.cs b/Prometheus/MetricFactory.cs new file mode 100644 index 00000000..db330d63 --- /dev/null +++ b/Prometheus/MetricFactory.cs @@ -0,0 +1,177 @@ +using static Prometheus.CollectorRegistry; + +namespace Prometheus; + +/// +/// Adds metrics to a registry. +/// +public sealed class MetricFactory : IMetricFactory +{ + private readonly CollectorRegistry _registry; + + // If set, these labels will be applied to all created metrics, acting as additional static labels scoped to this factory. + // These are appended to the metric-specific static labels set at metric creation time. + private readonly LabelSequence _factoryLabels; + + // Both the factory-defined and the registry-defined static labels. + // TODO: We should validate that registry labels cannot be defined any more once we have already resolved this. + private readonly Lazy _staticLabelsLazy; + + internal MetricFactory(CollectorRegistry registry) : this(registry, LabelSequence.Empty) + { + } + + internal MetricFactory(CollectorRegistry registry, in LabelSequence withLabels) + { + _registry = registry ?? throw new ArgumentNullException(nameof(registry)); + _factoryLabels = withLabels; + + _staticLabelsLazy = new Lazy(ResolveStaticLabels); + } + + private LabelSequence ResolveStaticLabels() + { + if (_factoryLabels.Length != 0) + return _factoryLabels.Concat(_registry.GetStaticLabels()); + else + return _registry.GetStaticLabels(); + } + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) + => CreateCounter(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) + => CreateGauge(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) + => CreateSummary(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); + + /// + /// Histograms track the size and number of events in buckets. + /// + public Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) + => CreateHistogram(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) + => CreateCounter(name, help, StringSequence.From(labelNames), configuration); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) + => CreateGauge(name, help, StringSequence.From(labelNames), configuration); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) + => CreateHistogram(name, help, StringSequence.From(labelNames), configuration); + + /// + /// Histograms track the size and number of events in buckets. + /// + public Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) + => CreateSummary(name, help, StringSequence.From(labelNames), configuration); + + internal Counter CreateCounter(string name, string help, StringSequence instanceLabelNames, CounterConfiguration? configuration) + { + var exemplarBehavior = configuration?.ExemplarBehavior ?? ExemplarBehavior ?? ExemplarBehavior.Default; + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default, exemplarBehavior, _createCounterInstanceFunc); + } + + internal Gauge CreateGauge(string name, string help, StringSequence instanceLabelNames, GaugeConfiguration? configuration) + { + // Note: exemplars are not supported for gauges. We just pass it along here to avoid forked APIs downstream. + var exemplarBehavior = ExemplarBehavior ?? ExemplarBehavior.Default; + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default, exemplarBehavior, _createGaugeInstanceFunc); + } + + internal Histogram CreateHistogram(string name, string help, StringSequence instanceLabelNames, HistogramConfiguration? configuration) + { + var exemplarBehavior = configuration?.ExemplarBehavior ?? ExemplarBehavior ?? ExemplarBehavior.Default; + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default, exemplarBehavior, _createHistogramInstanceFunc); + } + + internal Summary CreateSummary(string name, string help, StringSequence instanceLabelNames, SummaryConfiguration? configuration) + { + // Note: exemplars are not supported for summaries. We just pass it along here to avoid forked APIs downstream. + var exemplarBehavior = ExemplarBehavior ?? ExemplarBehavior.Default; + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default, exemplarBehavior, _createSummaryInstanceFunc); + } + + private static Counter CreateCounterInstance(string Name, string Help, in StringSequence InstanceLabelNames, in LabelSequence StaticLabels, CounterConfiguration Configuration, ExemplarBehavior ExemplarBehavior) => new(Name, Help, InstanceLabelNames, StaticLabels, Configuration.SuppressInitialValue, ExemplarBehavior); + + private static Gauge CreateGaugeInstance(string Name, string Help, in StringSequence InstanceLabelNames, in LabelSequence StaticLabels, GaugeConfiguration Configuration, ExemplarBehavior ExemplarBehavior) => new(Name, Help, InstanceLabelNames, StaticLabels, Configuration.SuppressInitialValue, ExemplarBehavior); + + private static Histogram CreateHistogramInstance(string Name, string Help, in StringSequence InstanceLabelNames, in LabelSequence StaticLabels, HistogramConfiguration Configuration, ExemplarBehavior ExemplarBehavior) => new(Name, Help, InstanceLabelNames, StaticLabels, Configuration.SuppressInitialValue, Configuration.Buckets, ExemplarBehavior); + + private static Summary CreateSummaryInstance(string name, string help, in StringSequence instanceLabelNames, in LabelSequence staticLabels, SummaryConfiguration configuration, ExemplarBehavior exemplarBehavior) => new(name, help, instanceLabelNames, staticLabels, exemplarBehavior, configuration.SuppressInitialValue, + configuration.Objectives, configuration.MaxAge, configuration.AgeBuckets, configuration.BufferSize); + + private static readonly CollectorInitializer _createCounterInstanceFunc = CreateCounterInstance; + private static readonly CollectorInitializer _createGaugeInstanceFunc = CreateGaugeInstance; + private static readonly CollectorInitializer _createHistogramInstanceFunc = CreateHistogramInstance; + private static readonly CollectorInitializer _createSummaryInstanceFunc = CreateSummaryInstance; + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public Counter CreateCounter(string name, string help, params string[] labelNames) => CreateCounter(name, help, labelNames, null); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public Gauge CreateGauge(string name, string help, params string[] labelNames) => CreateGauge(name, help, labelNames, null); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public Summary CreateSummary(string name, string help, params string[] labelNames) => CreateSummary(name, help, labelNames, null); + + /// + /// Histograms track the size and number of events in buckets. + /// + public Histogram CreateHistogram(string name, string help, params string[] labelNames) => CreateHistogram(name, help, labelNames, null); + + public IMetricFactory WithLabels(IDictionary labels) + { + if (labels.Count == 0) + return this; + + var newLabels = LabelSequence.From(labels); + + // Add any already-inherited labels to the end (rule is that lower levels go first, higher levels last). + var newFactoryLabels = newLabels.Concat(_factoryLabels); + + return new MetricFactory(_registry, newFactoryLabels); + } + + /// + /// Gets all the existing label names predefined either in the factory or in the registry. + /// + internal StringSequence GetAllStaticLabelNames() + { + return _factoryLabels.Names.Concat(_registry.GetStaticLabels().Names); + } + + public IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => + new ManagedLifetimeMetricFactory(this, expiresAfter); + + public ExemplarBehavior? ExemplarBehavior { get; set; } +} \ No newline at end of file diff --git a/Prometheus/MetricHandler.cs b/Prometheus/MetricHandler.cs new file mode 100644 index 00000000..8286bb2c --- /dev/null +++ b/Prometheus/MetricHandler.cs @@ -0,0 +1,69 @@ +namespace Prometheus; + +/// +/// Base class for various metric server implementations that start an independent exporter in the background. +/// The expoters may either be pull-based (exposing the Prometheus API) or push-based (actively pushing to PushGateway). +/// +public abstract class MetricHandler : IMetricServer, IDisposable +{ + // The token is cancelled when the handler is instructed to stop. + private CancellationTokenSource? _cts = new CancellationTokenSource(); + + // This is the task started for the purpose of exporting metrics. + private Task? _task; + + protected MetricHandler() + { + } + + public IMetricServer Start() + { + if (_task != null) + throw new InvalidOperationException("The metric server has already been started."); + + if (_cts == null) + throw new InvalidOperationException("The metric server has already been started and stopped. Create a new server if you want to start it again."); + + _task = StartServer(_cts.Token); + return this; + } + + public async Task StopAsync() + { + // Signal the CTS to give a hint to the server thread that it is time to close up shop. + _cts?.Cancel(); + + try + { + if (_task == null) + return; // Never started. + + // This will re-throw any exception that was caught on the StartServerAsync thread. + // Perhaps not ideal behavior but hey, if the implementation does not want this to happen + // it should have caught it itself in the background processing thread. + await _task.ConfigureAwait(false); // Issue #308 + } + catch (OperationCanceledException) + { + // We'll eat this one, though, since it can easily get thrown by whatever checks the CancellationToken. + } + finally + { + _cts?.Dispose(); + _cts = null; + } + } + + public void Stop() + { + // This method mainly exists for API compatiblity with prometheus-net v1. But it works, so that's fine. + StopAsync().GetAwaiter().GetResult(); + } + + public void Dispose() + { + Stop(); + } + + protected abstract Task StartServer(CancellationToken cancel); +} diff --git a/Prometheus/MetricPusher.cs b/Prometheus/MetricPusher.cs new file mode 100644 index 00000000..da9d0a1c --- /dev/null +++ b/Prometheus/MetricPusher.cs @@ -0,0 +1,179 @@ +using System.Diagnostics; +using System.Text; + +namespace Prometheus; + +/// +/// A metric server that regularly pushes metrics to a Prometheus PushGateway. +/// +public class MetricPusher : MetricHandler +{ + private readonly TimeSpan _pushInterval; + private readonly HttpMethod _method; + private readonly Uri _targetUrl; + private readonly Func _httpClientProvider; + + public MetricPusher(string endpoint, string job, string? instance = null, long intervalMilliseconds = 1000, IEnumerable>? additionalLabels = null, CollectorRegistry? registry = null, bool pushReplace = false) : this(new MetricPusherOptions + { + Endpoint = endpoint, + Job = job, + Instance = instance, + IntervalMilliseconds = intervalMilliseconds, + AdditionalLabels = additionalLabels, + Registry = registry, + ReplaceOnPush = pushReplace, + }) + { + } + + public MetricPusher(MetricPusherOptions options) + { + if (string.IsNullOrEmpty(options.Endpoint)) + throw new ArgumentNullException(nameof(options.Endpoint)); + + if (string.IsNullOrEmpty(options.Job)) + throw new ArgumentNullException(nameof(options.Job)); + + if (options.IntervalMilliseconds <= 0) + throw new ArgumentException("Interval must be greater than zero", nameof(options.IntervalMilliseconds)); + + _registry = options.Registry ?? Metrics.DefaultRegistry; + + _httpClientProvider = options.HttpClientProvider ?? (() => _singletonHttpClient); + + StringBuilder sb = new StringBuilder(string.Format("{0}/job/{1}", options.Endpoint!.TrimEnd('/'), options.Job)); + if (!string.IsNullOrEmpty(options.Instance)) + sb.AppendFormat("/instance/{0}", options.Instance); + + if (options.AdditionalLabels != null) + { + foreach (var pair in options.AdditionalLabels) + { + if (pair == null || string.IsNullOrEmpty(pair.Item1) || string.IsNullOrEmpty(pair.Item2)) + throw new NotSupportedException($"Invalid {nameof(MetricPusher)} additional label: ({pair?.Item1}):({pair?.Item2})"); + + sb.AppendFormat("/{0}/{1}", pair.Item1, pair.Item2); + } + } + + if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out var targetUrl) || targetUrl == null) + { + throw new ArgumentException("Endpoint must be a valid url", nameof(options.Endpoint)); + } + + _targetUrl = targetUrl; + + _pushInterval = TimeSpan.FromMilliseconds(options.IntervalMilliseconds); + _onError = options.OnError; + + _method = options.ReplaceOnPush ? HttpMethod.Put : HttpMethod.Post; + } + + private static readonly HttpClient _singletonHttpClient = new(); + + private readonly CollectorRegistry _registry; + private readonly Action? _onError; + + protected override Task StartServer(CancellationToken cancel) + { + // Start the server processing loop asynchronously in the background. + return Task.Run(async delegate + { + // We do 1 final push after we get cancelled, to ensure that we publish the final state. + var pushingFinalState = false; + + while (true) + { + // We schedule approximately at the configured interval. There may be some small accumulation for the + // part of the loop we do not measure but it is close enough to be acceptable for all practical scenarios. + var duration = ValueStopwatch.StartNew(); + + try + { + var httpClient = _httpClientProvider(); + + var request = new HttpRequestMessage + { + Method = _method, + RequestUri = _targetUrl, + // We use a copy-pasted implementation of PushStreamContent here to avoid taking a dependency on the old ASP.NET Web API where it lives. + Content = new PushStreamContentInternal(async (stream, content, context) => + { + try + { + // Do not pass CT because we only want to cancel after pushing, so a flush is always performed. + await _registry.CollectAndExportAsTextAsync(stream, default); + } + finally + { + stream.Close(); + } + }, PrometheusConstants.ExporterContentTypeValue), + }; + + var response = await httpClient.SendAsync(request); + + // If anything goes wrong, we want to get at least an entry in the trace log. + response.EnsureSuccessStatusCode(); + } + catch (ScrapeFailedException ex) + { + // We do not consider failed scrapes a reportable error since the user code that raises the failure should be the one logging it. + Trace.WriteLine($"Skipping metrics push due to failed scrape: {ex.Message}"); + } + catch (Exception ex) + { + HandleFailedPush(ex); + } + + if (cancel.IsCancellationRequested) + { + if (!pushingFinalState) + { + // Continue for one more loop to push the final state. + // We do this because it might be that we were stopped while in the middle of a push. + pushingFinalState = true; + continue; + } + else + { + // Final push completed, time to pack up our things and go home. + break; + } + } + + var sleepTime = _pushInterval - duration.GetElapsedTime(); + + // Sleep until the interval elapses or the pusher is asked to shut down. + if (sleepTime > TimeSpan.Zero) + { + try + { + await Task.Delay(sleepTime, cancel); + } + catch (OperationCanceledException) when (cancel.IsCancellationRequested) + { + // The task was cancelled. + // We continue the loop here to ensure final state gets pushed. + pushingFinalState = true; + continue; + } + } + } + }); + } + + private void HandleFailedPush(Exception ex) + { + if (_onError != null) + { + // Asynchronous because we don't trust the callee to be fast. + Task.Run(() => _onError(ex)); + } + else + { + // If there is no error handler registered, we write to trace to at least hopefully get some attention to the problem. + Trace.WriteLine(string.Format("Error in MetricPusher: {0}", ex)); + } + } +} diff --git a/Prometheus/MetricPusherOptions.cs b/Prometheus/MetricPusherOptions.cs new file mode 100644 index 00000000..c9214af2 --- /dev/null +++ b/Prometheus/MetricPusherOptions.cs @@ -0,0 +1,32 @@ +namespace Prometheus; + +public sealed class MetricPusherOptions +{ + internal static readonly MetricPusherOptions Default = new(); + + public string? Endpoint { get; set; } + public string? Job { get; set; } + public string? Instance { get; set; } + public long IntervalMilliseconds { get; set; } = 1000; + public IEnumerable>? AdditionalLabels { get; set; } + public CollectorRegistry? Registry { get; set; } + + /// + /// Callback for when a metric push fails. + /// + public Action? OnError { get; set; } + + /// + /// If null, a singleton HttpClient will be used. + /// + public Func? HttpClientProvider { get; set; } + + /// + /// If true, replace the metrics in the group (identified by Job, Instance, AdditionalLabels). + /// + /// Replace means a HTTP PUT request will be made, otherwise a HTTP POST request will be made (which means add metrics to the group, if it already exists). + /// + /// Note: Other implementations of the pushgateway client default to replace, however to preserve backwards compatibility this implementation defaults to add. + /// + public bool ReplaceOnPush { get; set; } = false; +} diff --git a/Prometheus/MetricServer.cs b/Prometheus/MetricServer.cs new file mode 100644 index 00000000..7b28eb4d --- /dev/null +++ b/Prometheus/MetricServer.cs @@ -0,0 +1,128 @@ +using System.Diagnostics; +using System.Net; + +namespace Prometheus; + +/// +/// Implementation of a Prometheus exporter that serves metrics using HttpListener. +/// This is a stand-alone exporter for apps that do not already have an HTTP server included. +/// +public class MetricServer : MetricHandler +{ + private readonly HttpListener _httpListener = new(); + + /// + /// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks. + /// By default (if null), all requests are served. + /// + public Func? RequestPredicate { get; set; } + + public MetricServer(int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : this("+", port, url, registry, useHttps) + { + } + + public MetricServer(string hostname, int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) + { + var s = useHttps ? "s" : ""; + _httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}"); + + _registry = registry ?? Metrics.DefaultRegistry; + } + + private readonly CollectorRegistry _registry; + + protected override Task StartServer(CancellationToken cancel) + { + // This will ensure that any failures to start are nicely thrown from StartServerAsync. + _httpListener.Start(); + + // Kick off the actual processing to a new thread and return a Task for the processing thread. + return Task.Factory.StartNew(delegate + { + try + { + Thread.CurrentThread.Name = "Metric Server"; //Max length 16 chars (Linux limitation) + + while (!cancel.IsCancellationRequested) + { + // There is no way to give a CancellationToken to GCA() so, we need to hack around it a bit. + var getContext = _httpListener.GetContextAsync(); + getContext.Wait(cancel); + var context = getContext.Result; + + // Asynchronously process the request. + _ = Task.Factory.StartNew(async delegate + { + var request = context.Request; + var response = context.Response; + + try + { + var predicate = RequestPredicate; + + if (predicate != null && !predicate(request)) + { + // Request rejected by predicate. + response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } + + try + { + // We first touch the response.OutputStream only in the callback because touching + // it means we can no longer send headers (the status code). + var serializer = new TextSerializer(delegate + { + response.ContentType = PrometheusConstants.TextContentTypeWithVersionAndEncoding; + response.StatusCode = 200; + return response.OutputStream; + }); + + await _registry.CollectAndSerializeAsync(serializer, cancel); + response.OutputStream.Dispose(); + } + catch (ScrapeFailedException ex) + { + // This can only happen before anything is written to the stream, so it + // should still be safe to update the status code and report an error. + response.StatusCode = 503; + + if (!string.IsNullOrWhiteSpace(ex.Message)) + { + using (var writer = new StreamWriter(response.OutputStream)) + writer.Write(ex.Message); + } + } + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + if (!_httpListener.IsListening) + return; // We were shut down. + + Trace.WriteLine(string.Format("Error in {0}: {1}", nameof(MetricServer), ex)); + + try + { + response.StatusCode = 500; + } + catch + { + // Might be too late in request processing to set response code, so just ignore. + } + } + finally + { + response.Close(); + } + }); + } + } + finally + { + _httpListener.Stop(); + // This should prevent any currently processed requests from finishing. + _httpListener.Close(); + } + }, TaskCreationOptions.LongRunning); + } +} diff --git a/Prometheus/MetricType.cs b/Prometheus/MetricType.cs new file mode 100644 index 00000000..aa844296 --- /dev/null +++ b/Prometheus/MetricType.cs @@ -0,0 +1,9 @@ +namespace Prometheus; + +internal enum MetricType +{ + Counter, + Gauge, + Summary, + Histogram +} diff --git a/Prometheus/Metrics.cs b/Prometheus/Metrics.cs new file mode 100644 index 00000000..41851cf4 --- /dev/null +++ b/Prometheus/Metrics.cs @@ -0,0 +1,178 @@ +namespace Prometheus; + +/// +/// Static class for easy creation of metrics. Acts as the entry point to the prometheus-net metrics recording API. +/// +/// Some built-in metrics are registered by default in the default collector registry. If these default metrics are +/// not desired, call to remove them before registering your own. +/// +public static class Metrics +{ + /// + /// The default registry where all metrics are registered by default. + /// + public static CollectorRegistry DefaultRegistry { get; private set; } + + /// + /// The default metric factory used to create collectors in the default registry. + /// + public static MetricFactory DefaultFactory { get; private set; } + + /// + /// Creates a new registry. You may want to use multiple registries if you want to + /// export different sets of metrics via different exporters (e.g. on different URLs). + /// + public static CollectorRegistry NewCustomRegistry() => new(); + + /// + /// Returns an instance of that you can use to register metrics in a custom registry. + /// + public static MetricFactory WithCustomRegistry(CollectorRegistry registry) => new(registry); + + /// + /// Adds the specified static labels to all metrics created using the returned factory. + /// + public static IMetricFactory WithLabels(IDictionary labels) => + new MetricFactory(DefaultRegistry, LabelSequence.From(labels)); + + /// + /// Returns a factory that creates metrics with a managed lifetime. + /// + /// + /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. + /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. + /// + public static IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => + DefaultFactory.WithManagedLifetime(expiresAfter); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public static Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) => + DefaultFactory.CreateCounter(name, help, configuration); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public static Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) => + DefaultFactory.CreateGauge(name, help, configuration); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public static Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) => + DefaultFactory.CreateSummary(name, help, configuration); + + /// + /// Histograms track the size and number of events in buckets. + /// + public static Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) => + DefaultFactory.CreateHistogram(name, help, configuration); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public static Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) => + DefaultFactory.CreateCounter(name, help, labelNames, configuration); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public static Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) => + DefaultFactory.CreateGauge(name, help, labelNames, configuration); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public static Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) => + DefaultFactory.CreateSummary(name, help, labelNames, configuration); + + /// + /// Histograms track the size and number of events in buckets. + /// + public static Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) => + DefaultFactory.CreateHistogram(name, help, labelNames, configuration); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public static Counter CreateCounter(string name, string help, params string[] labelNames) => + DefaultFactory.CreateCounter(name, help, labelNames); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public static Gauge CreateGauge(string name, string help, params string[] labelNames) => + DefaultFactory.CreateGauge(name, help, labelNames); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public static Summary CreateSummary(string name, string help, params string[] labelNames) => + DefaultFactory.CreateSummary(name, help, labelNames); + + /// + /// Histograms track the size and number of events in buckets. + /// + public static Histogram CreateHistogram(string name, string help, params string[] labelNames) => + DefaultFactory.CreateHistogram(name, help, labelNames); + + static Metrics() + { + DefaultRegistry = new CollectorRegistry(); + + // Configures defaults to their default behaviors, can be overridden by user if they desire (before first collection). + SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressNone); + + DefaultFactory = new MetricFactory(DefaultRegistry); + } + + /// + /// Suppresses the registration of the default sample metrics from the default registry. + /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). + /// + public static void SuppressDefaultMetrics() => SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressAll); + + /// + /// Suppresses the registration of the default sample metrics from the default registry. + /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). + /// + public static void SuppressDefaultMetrics(SuppressDefaultMetricOptions options) + { + options ??= SuppressDefaultMetricOptions.SuppressAll; + + // Only has effect if called before the registry is collected from. Otherwise a no-op. + DefaultRegistry.SetBeforeFirstCollectCallback(delegate + { + var configureCallbacks = new SuppressDefaultMetricOptions.ConfigurationCallbacks() + { +#if NET + ConfigureEventCounterAdapter = _configureEventCounterAdapterCallback, +#endif +#if NET6_0_OR_GREATER + ConfigureMeterAdapter = _configureMeterAdapterOptions +#endif + }; + + options.ApplyToDefaultRegistry(configureCallbacks); + }); + } + +#if NET + private static Action _configureEventCounterAdapterCallback = delegate { }; + + /// + /// Configures the event counter adapter that is enabled by default on startup. + /// + public static void ConfigureEventCounterAdapter(Action callback) => _configureEventCounterAdapterCallback = callback; +#endif + +#if NET6_0_OR_GREATER + private static Action _configureMeterAdapterOptions = delegate { }; + + /// + /// Configures the meter adapter that is enabled by default on startup. + /// + public static void ConfigureMeterAdapter(Action callback) => _configureMeterAdapterOptions = callback; +#endif +} \ No newline at end of file diff --git a/Prometheus/NonCapturingLazyInitializer.cs b/Prometheus/NonCapturingLazyInitializer.cs new file mode 100644 index 00000000..67b23d40 --- /dev/null +++ b/Prometheus/NonCapturingLazyInitializer.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Prometheus; + +// Copy-pasted from https://github.com/dotnet/efcore/blob/main/src/Shared/NonCapturingLazyInitializer.cs +// Crudely modified to inline dependencies and reduce functionality down to .NET Fx compatible level. +internal static class NonCapturingLazyInitializer +{ + public static TValue EnsureInitialized( + ref TValue? target, + TParam param, + Func valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, valueFactory(param), null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TParam1 param1, + TParam2 param2, + Func valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, valueFactory(param1, param2), null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TParam1 param1, + TParam2 param2, + TParam3 param3, + Func valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, valueFactory(param1, param2, param3), null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue target, + ref bool initialized, + TParam param, + Func valueFactory) + where TValue : class? + { + var alreadyInitialized = Volatile.Read(ref initialized); + if (alreadyInitialized) + { + var value = Volatile.Read(ref target); + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + DebugAssert(value != null, $"value was null in {nameof(EnsureInitialized)} after check"); + return value; + } + + Volatile.Write(ref target, valueFactory(param)); + Volatile.Write(ref initialized, true); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TValue value) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, value, null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TParam param, + Action valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + valueFactory(param); + + var tmp2 = Volatile.Read(ref target); + DebugAssert( + target != null && tmp2 != null, + $"{nameof(valueFactory)} did not initialize {nameof(target)} in {nameof(EnsureInitialized)}"); +#pragma warning disable CS8603 // Possible null reference return. + return tmp2; +#pragma warning restore CS8603 // Possible null reference return. + } + + [Conditional("DEBUG")] + private static void DebugAssert(bool condition, string message) + { + if (!condition) + { + throw new Exception($"Check.DebugAssert failed: {message}"); + } + } +} diff --git a/Prometheus/NoopDisposable.cs b/Prometheus/NoopDisposable.cs new file mode 100644 index 00000000..04364cb6 --- /dev/null +++ b/Prometheus/NoopDisposable.cs @@ -0,0 +1,8 @@ +namespace Prometheus; + +internal sealed class NoopDisposable : IDisposable +{ + public void Dispose() + { + } +} diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs new file mode 100644 index 00000000..3d82ef8d --- /dev/null +++ b/Prometheus/ObservedExemplar.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.ObjectPool; +using System.Diagnostics; + +namespace Prometheus; + +/// +/// Internal representation of an Exemplar ready to be serialized. +/// +internal sealed class ObservedExemplar +{ + /// + /// OpenMetrics places a length limit of 128 runes on the exemplar (sum of all key value pairs). + /// + private const int MaxRunes = 128; + + /// + /// We have a pool of unused instances that we can reuse, to avoid constantly allocating memory. Once the set of metrics stabilizes, + /// all allocations should generally be coming from the pool. We expect the default pool configuratiopn to be suitable for this. + /// + private static readonly ObjectPool Pool = ObjectPool.Create(); + + public static readonly ObservedExemplar Empty = new(); + + internal static Func NowProvider = DefaultNowProvider; + internal static double DefaultNowProvider() => LowGranularityTimeSource.GetSecondsFromUnixEpoch(); + + public Exemplar? Labels { get; private set; } + public double Value { get; private set; } + public double Timestamp { get; private set; } + + public ObservedExemplar() + { + Labels = null; + Value = 0; + Timestamp = 0; + } + + public bool IsValid => Labels != null; + + private void Update(Exemplar labels, double value) + { + Debug.Assert(this != Empty, "Do not mutate the sentinel"); + + var totalRuneCount = 0; + + for (var i = 0; i < labels.Length; i++) + { + totalRuneCount += labels[i].RuneCount; + for (var j = 0; j < labels.Length; j++) + { + if (i == j) continue; + if (ByteArraysEqual(labels[i].KeyBytes, labels[j].KeyBytes)) + throw new ArgumentException("Exemplar contains duplicate keys."); + } + } + + if (totalRuneCount > MaxRunes) + throw new ArgumentException($"Exemplar consists of {totalRuneCount} runes, exceeding the OpenMetrics limit of {MaxRunes}."); + + Labels = labels; + Value = value; + Timestamp = NowProvider(); + } + + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) return false; + + for (var i = 0; i < a.Length; i++) + if (a[i] != b[i]) return false; + + return true; + } + + /// + /// Takes ownership of the labels and will destroy them when the instance is returned to the pool. + /// + public static ObservedExemplar CreatePooled(Exemplar labels, double value) + { + var instance = Pool.Get(); + instance.Update(labels, value); + return instance; + } + + public static void ReturnPooledIfNotEmpty(ObservedExemplar instance) + { + if (object.ReferenceEquals(instance, Empty)) + return; // We never put the "Empty" instance into the pool. Do the check here to avoid repeating it any time we return instances to the pool. + + instance.Labels?.ReturnToPoolIfNotEmpty(); + Pool.Return(instance); + } +} \ No newline at end of file diff --git a/Prometheus/PlatformCompatibilityHelpers.cs b/Prometheus/PlatformCompatibilityHelpers.cs new file mode 100644 index 00000000..6069e4e7 --- /dev/null +++ b/Prometheus/PlatformCompatibilityHelpers.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace Prometheus; + +internal class PlatformCompatibilityHelpers +{ + // Reimplementation of Stopwatch.GetElapsedTime (only available on .NET 7 or newer). + public static TimeSpan StopwatchGetElapsedTime(long start, long end) + => new((long)((end - start) * ((double)10_000_000 / Stopwatch.Frequency))); + + public static long ElapsedToTimeStopwatchTicks(TimeSpan elapsedTime) + => (long)(elapsedTime.Ticks * (Stopwatch.Frequency / (double)10_000_000)); +} diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj new file mode 100644 index 00000000..addc6192 --- /dev/null +++ b/Prometheus/Prometheus.csproj @@ -0,0 +1,89 @@ + + + + net6.0;net7.0;netstandard2.0 + + + net462;net6.0;net7.0;netstandard2.0 + + + + + Prometheus.NetStandard + Prometheus + + false + + true + ..\Resources\prometheus-net.snk + + enable + enable + True + True + 1591 + + true + + + preview + 9999 + + True + + + true + true + + + prometheus-net + andrasm,qed-,lakario,sandersaares + prometheus-net + prometheus-net + .NET client library for the Prometheus monitoring and alerting system + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + metrics prometheus + MIT + True + snupkg + + + + + true + + + + + + + + + + + + + + True + \ + + + True + \ + + + + + + + + + + diff --git a/Prometheus/PrometheusConstants.cs b/Prometheus/PrometheusConstants.cs new file mode 100644 index 00000000..bcff4471 --- /dev/null +++ b/Prometheus/PrometheusConstants.cs @@ -0,0 +1,22 @@ +using System.Net.Http.Headers; +using System.Text; + +namespace Prometheus; + +public static class PrometheusConstants +{ + public const string TextContentType = "text/plain"; + public const string OpenMetricsContentType = "application/openmetrics-text"; + + public const string TextContentTypeWithVersionAndEncoding = TextContentType + "; version=0.0.4; charset=utf-8"; + public const string OpenMetricsContentTypeWithVersionAndEncoding = OpenMetricsContentType + "; version=1.0.0; charset=utf-8"; + + // ASP.NET requires a MediaTypeHeaderValue object + public static readonly MediaTypeHeaderValue ExporterContentTypeValue = MediaTypeHeaderValue.Parse(TextContentTypeWithVersionAndEncoding); + public static readonly MediaTypeHeaderValue ExporterOpenMetricsContentTypeValue = MediaTypeHeaderValue.Parse(OpenMetricsContentTypeWithVersionAndEncoding); + + // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never prepended to the output stream. + public static readonly Encoding ExportEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + internal static readonly Encoding ExemplarEncoding = new ASCIIEncoding(); +} \ No newline at end of file diff --git a/Prometheus/PrometheusNameHelpers.cs b/Prometheus/PrometheusNameHelpers.cs new file mode 100644 index 00000000..704ca57b --- /dev/null +++ b/Prometheus/PrometheusNameHelpers.cs @@ -0,0 +1,57 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Prometheus; + +/// +/// Transforms external names in different character sets into Prometheus (metric or label) names. +/// +internal static class PrometheusNameHelpers +{ + private static readonly Regex NameRegex = new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); + private const string FirstCharacterCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; + private const string NonFirstCharacterCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + + public static string TranslateNameToPrometheusName(string inputName) + { + // Transformations done: + // * all lowercase + // * special characters to underscore + // * must match: [a-zA-Z_][a-zA-Z0-9_]* + // * colon is "permitted" by spec but reserved for recording rules + + var sb = new StringBuilder(); + + foreach (char inputCharacter in inputName) + { + // All lowercase. + var c = Char.ToLowerInvariant(inputCharacter); + + if (sb.Length == 0) + { + // If first character is not from allowed charset, prefix it with underscore to minimize first character data loss. + if (!FirstCharacterCharset.Contains(c)) + sb.Append('_'); + + sb.Append(c); + } + else + { + // Standard rules. + // If character is not permitted, replace with underscore. Simple as that! + if (!NonFirstCharacterCharset.Contains(c)) + sb.Append('_'); + else + sb.Append(c); + } + } + + var name = sb.ToString(); + + // Sanity check. + if (!NameRegex.IsMatch(name)) + throw new Exception("Self-check failed: generated name did not match our own naming rules."); + + return name; + } +} diff --git a/Prometheus/PushStreamContentInternal.cs b/Prometheus/PushStreamContentInternal.cs new file mode 100644 index 00000000..eeb1da03 --- /dev/null +++ b/Prometheus/PushStreamContentInternal.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Simplified for prometheus-net for dependency reduction reasons. + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http.Headers; + +namespace Prometheus; + +/// +/// Provides an implementation that exposes an output +/// which can be written to directly. The ability to push data to the output stream differs from the +/// where data is pulled and not pushed. +/// +sealed class PushStreamContentInternal : HttpContent +{ + private readonly Func _onStreamAvailable; + + private static readonly MediaTypeHeaderValue OctetStreamHeaderValue = MediaTypeHeaderValue.Parse("application/octet-stream"); + + /// + /// Initializes a new instance of the class with the given . + /// + public PushStreamContentInternal(Func onStreamAvailable, MediaTypeHeaderValue mediaType) + { + _onStreamAvailable = onStreamAvailable; + Headers.ContentType = mediaType ?? OctetStreamHeaderValue; + } + + /// + /// When this method is called, it calls the action provided in the constructor with the output + /// stream to write to. Once the action has completed its work it closes the stream which will + /// close this content instance and complete the HTTP request or response. + /// + /// The to which to write. + /// The associated . + /// A instance that is asynchronously serializing the object's content. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")] + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + TaskCompletionSource serializeToStreamTask = new TaskCompletionSource(); + + Stream wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); + await _onStreamAvailable(wrappedStream, this, context); + + // wait for wrappedStream.Close/Dispose to get called. + await serializeToStreamTask.Task; + } + + /// + /// Computes the length of the stream if possible. + /// + /// The computed length of the stream. + /// true if the length has been computed; otherwise false. + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } + + internal class CompleteTaskOnCloseStream : DelegatingStreamInternal + { + private TaskCompletionSource _serializeToStreamTask; + + public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask) + : base(innerStream) + { + _serializeToStreamTask = serializeToStreamTask; + } + + [SuppressMessage( + "Microsoft.Usage", + "CA2215:Dispose methods should call base class dispose", + Justification = "See comments, this is intentional.")] + protected override void Dispose(bool disposing) + { + // We don't dispose the underlying stream because we don't own it. Dispose in this case just signifies + // that the user's action is finished. + _serializeToStreamTask.TrySetResult(true); + } + } +} diff --git a/Prometheus/QuantileEpsilonPair.cs b/Prometheus/QuantileEpsilonPair.cs new file mode 100644 index 00000000..52b2eb5e --- /dev/null +++ b/Prometheus/QuantileEpsilonPair.cs @@ -0,0 +1,7 @@ +namespace Prometheus; + +public readonly struct QuantileEpsilonPair(double quantile, double epsilon) +{ + public double Quantile { get; } = quantile; + public double Epsilon { get; } = epsilon; +} diff --git a/Prometheus/RealDelayer.cs b/Prometheus/RealDelayer.cs new file mode 100644 index 00000000..d31b9755 --- /dev/null +++ b/Prometheus/RealDelayer.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; + +namespace Prometheus; + +/// +/// An implementation that uses Task.Delay(), for use at runtime. +/// +internal sealed class RealDelayer : IDelayer +{ + public static readonly RealDelayer Instance = new(); + + [DebuggerStepThrough] + public Task Delay(TimeSpan duration) => Task.Delay(duration); + + [DebuggerStepThrough] + public Task Delay(TimeSpan duration, CancellationToken cancel) => Task.Delay(duration, cancel); +} diff --git a/Prometheus/RefLease.cs b/Prometheus/RefLease.cs new file mode 100644 index 00000000..17feabf1 --- /dev/null +++ b/Prometheus/RefLease.cs @@ -0,0 +1,21 @@ +namespace Prometheus; + +/// +/// A stack-only struct for holding a lease on a lifetime-managed metric. +/// Helps avoid allocation when you need to take a lease in a synchronous context where stack-only structs are allowed. +/// +public readonly ref struct RefLease +{ + internal RefLease(INotifyLeaseEnded notifyLeaseEnded, object child, ChildLifetimeInfo lifetime) + { + _notifyLeaseEnded = notifyLeaseEnded; + _child = child; + _lifetime = lifetime; + } + + private readonly INotifyLeaseEnded _notifyLeaseEnded; + private readonly object _child; + private readonly ChildLifetimeInfo _lifetime; + + public void Dispose() => _notifyLeaseEnded.OnLeaseEnded(_child, _lifetime); +} \ No newline at end of file diff --git a/Prometheus/ScrapeFailedException.cs b/Prometheus/ScrapeFailedException.cs new file mode 100644 index 00000000..833adc48 --- /dev/null +++ b/Prometheus/ScrapeFailedException.cs @@ -0,0 +1,15 @@ +namespace Prometheus; + +/// +/// Signals to the metrics server that metrics are currently unavailable. Thrown from "before collect" callbacks. +/// This causes the entire export operation to fail - even if some metrics are available, they will not be exported. +/// +/// The exception message will be delivered as the HTTP response body by the exporter. +/// +[Serializable] +public class ScrapeFailedException : Exception +{ + public ScrapeFailedException() { } + public ScrapeFailedException(string message) : base(message) { } + public ScrapeFailedException(string message, Exception inner) : base(message, inner) { } +} diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs new file mode 100644 index 00000000..74334106 --- /dev/null +++ b/Prometheus/StringSequence.cs @@ -0,0 +1,302 @@ +using System.Runtime.CompilerServices; + +namespace Prometheus; + +/// +/// Used for maintaining low-allocation-overhead ordered lists of strings, such as those used for metric label names and label values. +/// The list can be constructed from multiple segments that come from different types of data sources, unified into a single sequence by this type. +/// +/// +/// We assume (as an optimization) that the segments the sequence is made from never change. +/// We compare values using ordinal comparison. +/// +/// We explicitly do not mark this sequence as enumerable or a collection type, to prevent accidentally using a non-performance-tuned enumeration method. +/// +internal readonly struct StringSequence : IEquatable +{ + public static readonly StringSequence Empty = new(); + + public Enumerator GetEnumerator() => new(_values.Span, _inheritedValueArrays ?? []); + + public ref struct Enumerator + { + private int _completedItemsInValues; + private int _completedInheritedArrays; + private int _completedItemsInCurrentArray; + + private readonly ReadOnlySpan _values; + private readonly ReadOnlyMemory[] _inheritedValues; + + private ReadOnlySpan _currentArray; + private string _current; + + public readonly string Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _current; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator(ReadOnlySpan values, ReadOnlyMemory[] inheritedValues) + { + _values = values; + _inheritedValues = inheritedValues; + + _current = string.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + // Do we have an item to get from the primary values array? + if (_values.Length > _completedItemsInValues) + { + _current = _values[_completedItemsInValues]; + _completedItemsInValues++; + return true; + } + // Do we have an item to get from an inherited array? + else if (_inheritedValues.Length > _completedInheritedArrays) + { + if (_completedItemsInCurrentArray == 0) + _currentArray = _inheritedValues[_completedInheritedArrays].Span; + + _current = _currentArray[_completedItemsInCurrentArray++]; + + // Did we complete this array? + if (_currentArray.Length == _completedItemsInCurrentArray) + { + _completedItemsInCurrentArray = 0; + _completedInheritedArrays++; + } + + return true; + } + else + { + // All done! + return false; + } + } + } + + public int Length { get; } + + public bool IsEmpty => Length == 0; + + public bool Equals(StringSequence other) + { + if (_hashCode != other._hashCode) return false; + if (Length != other.Length) return false; + + var left = GetEnumerator(); + var right = other.GetEnumerator(); + + for (var i = 0; i < Length; i++) + { + if (!left.MoveNext()) throw new Exception("API contract violation."); + if (!right.MoveNext()) throw new Exception("API contract violation."); + + if (!string.Equals(left.Current, right.Current, StringComparison.Ordinal)) + return false; + } + + return true; + } + + public override bool Equals(object? obj) + { + if (obj is StringSequence ss) + return Equals(ss); + + return false; + } + + public override int GetHashCode() => _hashCode; + + // There are various ways we can make a StringSequence, comining one or two parents and maybe adding some extra to the start. + // This ctor tries to account for all these options. + private StringSequence(StringSequence inheritFrom, StringSequence thenFrom, in ReadOnlyMemory andFinallyPrepend) + { + // Anything inherited is already validated. Perform a sanity check on anything new. + if (andFinallyPrepend.Length != 0) + { + var span = andFinallyPrepend.Span; + + for (var i = 0; i < span.Length; i++) + { + if (span[i] == null) + throw new NotSupportedException("Null values are not supported for metric label names and values."); + } + + _values = andFinallyPrepend; + } + + if (!inheritFrom.IsEmpty || !thenFrom.IsEmpty) + _inheritedValueArrays = InheritFrom(inheritFrom, thenFrom); + + Length = _values.Length + inheritFrom.Length + thenFrom.Length; + + _hashCode = CalculateHashCode(); + } + + public static StringSequence From(params string[] values) + { + if (values.Length == 0) + return Empty; + + return new StringSequence(Empty, Empty, values); + } + + public static StringSequence From(ReadOnlyMemory values) + { + if (values.Length == 0) + return Empty; + + return new StringSequence(Empty, Empty, values); + } + + // Creates a new sequence, inheriting all current values and optionally adding more. New values are prepended to the sequence, inherited values come last. + public StringSequence InheritAndPrepend(params string[] prependValues) + { + if (prependValues.Length == 0) + return this; + + return new StringSequence(this, Empty, prependValues); + } + + // Creates a new sequence, inheriting all current values and optionally adding more. New values are prepended to the sequence, inherited values come last. + public StringSequence InheritAndPrepend(StringSequence prependValues) + { + if (prependValues.IsEmpty) + return this; + + if (IsEmpty) + return prependValues; + + return new StringSequence(this, prependValues, null); + } + + // Creates a new sequence, concatenating another string sequence (by inheriting from it). + public StringSequence Concat(StringSequence concatenatedValues) + { + if (concatenatedValues.IsEmpty) + return this; + + if (IsEmpty) + return concatenatedValues; + + return new StringSequence(concatenatedValues, this, null); + } + + // Values added by this instance. It may be empty. + private readonly ReadOnlyMemory _values; + + // Inherited values from one or more parent instances. + // It may be null because structs have a default ctor that zero-initializes them, so watch out. + private readonly ReadOnlyMemory[]? _inheritedValueArrays; + + private readonly int _hashCode; + + // We can inherit from one or two parent sequences. Order is "first at the end, second prefixed to it" as is typical (ancestors at the end). + private static ReadOnlyMemory[] InheritFrom(StringSequence first, StringSequence second) + { + // Expected output: second._values, second._inheritedValues, first._values, first._inheritedValues + + int firstOwnArrayCount = 0; + int firstInheritedArrayCount = 0; + int secondOwnArrayCount = 0; + int secondInheritedArrayCount = 0; + + if (!first.IsEmpty) + { + firstOwnArrayCount = first._values.Length > 0 ? 1 : 0; + firstInheritedArrayCount = first._inheritedValueArrays?.Length ?? 0; + } + + if (!second.IsEmpty) + { + secondOwnArrayCount = second._values.Length > 0 ? 1 : 0; + secondInheritedArrayCount = second._inheritedValueArrays?.Length ?? 0; + } + + var totalSegmentCount = firstOwnArrayCount + firstInheritedArrayCount + secondOwnArrayCount + secondInheritedArrayCount; + + if (totalSegmentCount == 0) + throw new Exception("Unreachable code reached: InheritFrom() should not even be called if there is nothing to inherit."); + + var result = new ReadOnlyMemory[totalSegmentCount]; + + var targetIndex = 0; + + if (secondOwnArrayCount != 0) + { + result[targetIndex++] = second._values; + } + + if (secondInheritedArrayCount != 0) + { + Array.Copy(second._inheritedValueArrays!, 0, result, targetIndex, secondInheritedArrayCount); + targetIndex += secondInheritedArrayCount; + } + + if (firstOwnArrayCount != 0) + { + result[targetIndex++] = first._values; + } + + if (firstInheritedArrayCount != 0) + { + Array.Copy(first._inheritedValueArrays!, 0, result, targetIndex, firstInheritedArrayCount); + } + + return result; + } + + private int CalculateHashCode() + { + int hashCode = 0; + + foreach (var item in this) + { + unchecked + { + hashCode ^= (item.GetHashCode() * 397); + } + } + + return hashCode; + } + + public bool Contains(string value) + { + foreach (var item in this) + { + if (item.Equals(value, StringComparison.Ordinal)) + return true; + } + + return false; + } + + /// + /// Copies the sequence to a new array. Try keep this out of any hot path at runtime, please! + /// + public string[] ToArray() + { + var result = new string[Length]; + + var index = 0; + + foreach (var item in this) + result[index++] = item; + + return result; + } + + public override string ToString() + { + // Just for debugging. + return $"({Length}) {string.Join(", ", ToArray())}"; + } +} diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs new file mode 100644 index 00000000..1b027c72 --- /dev/null +++ b/Prometheus/Summary.cs @@ -0,0 +1,343 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using Prometheus.SummaryImpl; + +namespace Prometheus; + +public sealed class Summary : Collector, ISummary +{ + // Label that defines the quantile in a summary. + private const string QuantileLabel = "quantile"; + + /// + /// Client library guidelines say that the summary should default to not measuring quantiles. + /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary + /// + internal static readonly QuantileEpsilonPair[] DefObjectivesArray = new QuantileEpsilonPair[0]; + + // Default duration for which observations stay relevant + public static readonly TimeSpan DefMaxAge = TimeSpan.FromMinutes(10); + + // Default number of buckets used to calculate the age of observations + public static readonly int DefAgeBuckets = 5; + + // Standard buffer size for collecting Summary observations + public static readonly int DefBufCap = 500; + + // Objectives defines the quantile rank estimates with their respective + // absolute error. If Objectives[q] = e, then the value reported + // for q will be the φ-quantile value for some φ between q-e and q+e. + // The default value is DefObjectives. + private readonly IReadOnlyList _objectives; + + // MaxAge defines the duration for which an observation stays relevant + // for the summary. Must be positive. The default value is DefMaxAge. + private readonly TimeSpan _maxAge; + + // AgeBuckets is the number of buckets used to exclude observations that + // are older than MaxAge from the summary. A higher number has a + // resource penalty, so only increase it if the higher resolution is + // really required. For very high observation rates, you might want to + // reduce the number of age buckets. With only one age bucket, you will + // effectively see a complete reset of the summary each time MaxAge has + // passed. The default value is DefAgeBuckets. + private readonly int _ageBuckets; + + // BufCap defines the default sample stream buffer size. The default + // value of DefBufCap should suffice for most uses. If there is a need + // to increase the value, a multiple of 500 is recommended (because that + // is the internal buffer size of the underlying package + // "github.com/bmizerany/perks/quantile"). + private readonly int _bufCap; + + private readonly double[] _sortedObjectives; + + // These labels go together with the objectives, so we do not need to allocate them for every child. + private readonly CanonicalLabel[] _quantileLabels; + + private static readonly byte[] QuantileLabelName = "quantile"u8.ToArray(); + + internal Summary( + string name, + string help, + StringSequence instanceLabelNames, + LabelSequence staticLabels, + ExemplarBehavior exemplarBehavior, + bool suppressInitialValue = false, + IReadOnlyList? objectives = null, + TimeSpan? maxAge = null, + int? ageBuckets = null, + int? bufCap = null) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) + { + _objectives = objectives ?? DefObjectivesArray; + _maxAge = maxAge ?? DefMaxAge; + _ageBuckets = ageBuckets ?? DefAgeBuckets; + _bufCap = bufCap ?? DefBufCap; + + if (_objectives.Count == 0) + _objectives = DefObjectivesArray; + + if (_maxAge < TimeSpan.Zero) + throw new ArgumentException($"Illegal max age {_maxAge}"); + + if (_ageBuckets == 0) + _ageBuckets = DefAgeBuckets; + + if (_bufCap == 0) + _bufCap = DefBufCap; + + if (instanceLabelNames.Contains(QuantileLabel)) + throw new ArgumentException($"{QuantileLabel} is a reserved label name"); + + if (_objectives.Count == 0) + { + _sortedObjectives = []; + _quantileLabels = []; + } + else + { + _sortedObjectives = new double[_objectives.Count]; + _quantileLabels = new CanonicalLabel[_objectives.Count]; + + for (var i = 0; i < _objectives.Count; i++) + { + _sortedObjectives[i] = _objectives[i].Quantile; + _quantileLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(QuantileLabelName, _objectives[i].Quantile); + } + + Array.Sort(_sortedObjectives); + } + } + + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + } + + internal override MetricType Type => MetricType.Summary; + + public sealed class Child : ChildBase, ISummary + { + internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) + { + _parent = parent; + + _hotBuf = new SampleBuffer(_parent._bufCap); + _coldBuf = new SampleBuffer(_parent._bufCap); + _streamDuration = new TimeSpan(_parent._maxAge.Ticks / _parent._ageBuckets); + _headStreamExpUnixtimeSeconds = LowGranularityTimeSource.GetSecondsFromUnixEpoch() + _streamDuration.TotalSeconds; + _hotBufExpUnixtimeSeconds = _headStreamExpUnixtimeSeconds; + + _streams = new QuantileStream[_parent._ageBuckets]; + for (var i = 0; i < _parent._ageBuckets; i++) + { + _streams[i] = QuantileStream.NewTargeted(_parent._objectives); + } + + _headStream = _streams[0]; + } + + private readonly Summary _parent; + + private static readonly byte[] SumSuffix = "sum"u8.ToArray(); + private static readonly byte[] CountSuffix = "count"u8.ToArray(); + +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) + { + // We output sum. + // We output count. + // We output quantiles. + + var now = LowGranularityTimeSource.GetSecondsFromUnixEpoch(); + + long count; + double sum; + + var values = ArrayPool<(double quantile, double value)>.Shared.Rent(_parent._objectives.Count); + var valuesIndex = 0; + + try + { + lock (_bufLock) + { + lock (_lock) + { + // Swap bufs even if hotBuf is empty to set new hotBufExpTime. + SwapBufs(now); + FlushColdBuf(); + + count = _count; + sum = _sum; + + for (var i = 0; i < _parent._sortedObjectives.Length; i++) + { + var quantile = _parent._sortedObjectives[i]; + var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile); + + values[valuesIndex++] = (quantile, value); + } + } + } + + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + sum, + ObservedExemplar.Empty, + SumSuffix, + cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + count, + ObservedExemplar.Empty, + CountSuffix, + cancel); + + for (var i = 0; i < _parent._objectives.Count; i++) + { + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + _parent._quantileLabels[i], + values[i].value, + ObservedExemplar.Empty, + null, + cancel); + } + } + finally + { + ArrayPool<(double quantile, double value)>.Shared.Return(values); + } + } + + private double _sum; + private long _count; + private SampleBuffer _hotBuf; + private SampleBuffer _coldBuf; + private readonly QuantileStream[] _streams; + private readonly TimeSpan _streamDuration; + private QuantileStream _headStream; + private int _headStreamIdx; + private double _headStreamExpUnixtimeSeconds; + private double _hotBufExpUnixtimeSeconds; + + // Protects hotBuf and hotBufExpTime. + private readonly object _bufLock = new(); + + // Protects every other moving part. + // Lock bufMtx before mtx if both are needed. + private readonly object _lock = new(); + + public void Observe(double val) + { + Observe(val, LowGranularityTimeSource.GetSecondsFromUnixEpoch()); + } + + /// + /// For unit tests only + /// + internal void Observe(double val, double nowUnixtimeSeconds) + { + if (double.IsNaN(val)) + return; + + lock (_bufLock) + { + if (nowUnixtimeSeconds > _hotBufExpUnixtimeSeconds) + Flush(nowUnixtimeSeconds); + + _hotBuf.Append(val); + + if (_hotBuf.IsFull) + Flush(nowUnixtimeSeconds); + } + + Publish(); + } + + // Flush needs bufMtx locked. + private void Flush(double nowUnixtimeSeconds) + { + lock (_lock) + { + SwapBufs(nowUnixtimeSeconds); + + // Go version flushes on a separate goroutine, but doing this on another + // thread actually makes the benchmark tests slower in .net + FlushColdBuf(); + } + } + + // SwapBufs needs mtx AND bufMtx locked, coldBuf must be empty. + private void SwapBufs(double nowUnixtimeSeconds) + { + if (!_coldBuf.IsEmpty) + throw new InvalidOperationException("coldBuf is not empty"); + + (_coldBuf, _hotBuf) = (_hotBuf, _coldBuf); + + // hotBuf is now empty and gets new expiration set. + while (nowUnixtimeSeconds > _hotBufExpUnixtimeSeconds) + { + _hotBufExpUnixtimeSeconds += _streamDuration.TotalSeconds; + } + } + + // FlushColdBuf needs mtx locked. + private void FlushColdBuf() + { + for (var bufIdx = 0; bufIdx < _coldBuf.Position; bufIdx++) + { + var value = _coldBuf[bufIdx]; + + for (var streamIdx = 0; streamIdx < _streams.Length; streamIdx++) + { + _streams[streamIdx].Insert(value); + } + + _count++; + _sum += value; + } + + _coldBuf.Reset(); + MaybeRotateStreams(); + } + + // MaybeRotateStreams needs mtx AND bufMtx locked. + private void MaybeRotateStreams() + { + while (!_hotBufExpUnixtimeSeconds.Equals(_headStreamExpUnixtimeSeconds)) + { + _headStream.Reset(); + _headStreamIdx++; + + if (_headStreamIdx >= _streams.Length) + _headStreamIdx = 0; + + _headStream = _streams[_headStreamIdx]; + _headStreamExpUnixtimeSeconds += _streamDuration.TotalSeconds; + } + } + } + + public void Observe(double val) + { + Unlabelled.Observe(val); + } + + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + // count + sum + objectives + internal override int TimeseriesCount => ChildCount * (2 + _objectives.Count); +} \ No newline at end of file diff --git a/Prometheus/SummaryConfiguration.cs b/Prometheus/SummaryConfiguration.cs new file mode 100644 index 00000000..e6162e97 --- /dev/null +++ b/Prometheus/SummaryConfiguration.cs @@ -0,0 +1,31 @@ +namespace Prometheus; + +public sealed class SummaryConfiguration : MetricConfiguration +{ + internal static readonly SummaryConfiguration Default = new SummaryConfiguration(); + + /// + /// Pairs of quantiles and allowed error values (epsilon). + /// + /// For example, a quantile of 0.95 with an epsilon of 0.01 means the calculated value + /// will be between the 94th and 96th quantile. + /// + /// If null, no quantiles will be calculated! + /// + public IReadOnlyList Objectives { get; set; } = Summary.DefObjectivesArray; + + /// + /// Time span over which to calculate the summary. + /// + public TimeSpan MaxAge { get; set; } = Summary.DefMaxAge; + + /// + /// Number of buckets used to control measurement expiration. + /// + public int AgeBuckets { get; set; } = Summary.DefAgeBuckets; + + /// + /// Buffer size limit. Use multiples of 500 to avoid waste, as internal buffers use that size. + /// + public int BufferSize { get; set; } = Summary.DefBufCap; +} diff --git a/Prometheus/SummaryImpl/QuantileStream.cs b/Prometheus/SummaryImpl/QuantileStream.cs new file mode 100644 index 00000000..bb34f7bf --- /dev/null +++ b/Prometheus/SummaryImpl/QuantileStream.cs @@ -0,0 +1,144 @@ +namespace Prometheus.SummaryImpl; + +// Ported from https://github.com/beorn7/perks/blob/master/quantile/stream.go + +// Package quantile computes approximate quantiles over an unbounded data +// stream within low memory and CPU bounds. +// +// A small amount of accuracy is traded to achieve the above properties. +// +// Multiple streams can be merged before calling Query to generate a single set +// of results. This is meaningful when the streams represent the same type of +// data. See Merge and Samples. +// +// For more detailed information about the algorithm used, see: +// +// Effective Computation of Biased Quantiles over Data Streams +// +// http://www.cs.rutgers.edu/~muthu/bquant.pdf + +internal delegate double Invariant(SampleStream stream, double r); + +internal sealed class QuantileStream +{ + private readonly SampleStream _sampleStream; + private readonly List _samples; + private bool _sorted; + + private QuantileStream(SampleStream sampleStream, List samples, bool sorted) + { + _sampleStream = sampleStream; + _samples = samples; + _sorted = sorted; + } + + public static QuantileStream NewStream(Invariant invariant) + { + return new QuantileStream(new SampleStream(invariant), new List { Capacity = 500 }, true); + } + + // NewTargeted returns an initialized Stream concerned with a particular set of + // quantile values that are supplied a priori. Knowing these a priori reduces + // space and computation time. The targets map maps the desired quantiles to + // their absolute errors, i.e. the true quantile of a value returned by a query + // is guaranteed to be within (Quantile±Epsilon). + // + // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. + public static QuantileStream NewTargeted(IReadOnlyList targets) + { + return NewStream((stream, r) => + { + var m = double.MaxValue; + + for (var i = 0; i < targets.Count; i++) + { + var target = targets[i]; + + double f; + if (target.Quantile * stream.N <= r) + f = (2 * target.Epsilon * r) / target.Quantile; + else + f = (2 * target.Epsilon * (stream.N - r)) / (1 - target.Quantile); + + if (f < m) + m = f; + } + + return m; + }); + } + + public void Insert(double value) + { + Insert(new Sample { Value = value, Width = 1 }); + } + + private void Insert(Sample sample) + { + _samples.Add(sample); + _sorted = false; + if (_samples.Count == _samples.Capacity) + Flush(); + } + + private void Flush() + { + MaybeSort(); + _sampleStream.Merge(_samples); + _samples.Clear(); + } + + private void MaybeSort() + { + if (!_sorted) + { + _sorted = true; + _samples.Sort(SampleComparison); + } + } + + private static int SampleComparison(Sample lhs, Sample rhs) + { + return lhs.Value.CompareTo(rhs.Value); + } + + public void Reset() + { + _sampleStream.Reset(); + _samples.Clear(); + } + + // Count returns the total number of samples observed in the stream since initialization. + public int Count => _samples.Count + _sampleStream.Count; + + public int SamplesCount => _samples.Count; + + public bool Flushed => _sampleStream.SampleCount > 0; + + // Query returns the computed qth percentiles value. If s was created with + // NewTargeted, and q is not in the set of quantiles provided a priori, Query + // will return an unspecified result. + public double Query(double q) + { + if (!Flushed) + { + // Fast path when there hasn't been enough data for a flush; + // this also yields better accuracy for small sets of data. + + var l = _samples.Count; + + if (l == 0) + return 0; + + var i = (int)(l * q); + if (i > 0) + i -= 1; + + MaybeSort(); + return _samples[i].Value; + } + + Flush(); + return _sampleStream.Query(q); + } +} diff --git a/Prometheus.NetStandard/SummaryImpl/Sample.cs b/Prometheus/SummaryImpl/Sample.cs similarity index 100% rename from Prometheus.NetStandard/SummaryImpl/Sample.cs rename to Prometheus/SummaryImpl/Sample.cs diff --git a/Prometheus/SummaryImpl/SampleBuffer.cs b/Prometheus/SummaryImpl/SampleBuffer.cs new file mode 100644 index 00000000..61fc531b --- /dev/null +++ b/Prometheus/SummaryImpl/SampleBuffer.cs @@ -0,0 +1,45 @@ +namespace Prometheus.SummaryImpl; + +internal sealed class SampleBuffer +{ + private readonly double[] _buffer; + + public SampleBuffer(int capacity) + { + if (capacity <= 0) + throw new ArgumentOutOfRangeException(nameof(capacity), "Must be > 0"); + + _buffer = new double[capacity]; + Position = 0; + } + + public void Append(double value) + { + if (Position >= Capacity) + throw new InvalidOperationException("Buffer is full"); + + _buffer[Position++] = value; + } + + public double this[int index] + { + get + { + if (index > Position) + throw new ArgumentOutOfRangeException(nameof(index), "Index is greater than position"); + + return _buffer[index]; + } + } + + public void Reset() + { + Position = 0; + } + + public int Position { get; private set; } + + public int Capacity => _buffer.Length; + public bool IsFull => Position == Capacity; + public bool IsEmpty => Position == 0; +} diff --git a/Prometheus/SummaryImpl/SampleStream.cs b/Prometheus/SummaryImpl/SampleStream.cs new file mode 100644 index 00000000..08ff2a84 --- /dev/null +++ b/Prometheus/SummaryImpl/SampleStream.cs @@ -0,0 +1,112 @@ +namespace Prometheus.SummaryImpl; + +internal sealed class SampleStream +{ + public double N; + private readonly List _samples = new List(); + private readonly Invariant _invariant; + + public SampleStream(Invariant invariant) + { + _invariant = invariant; + } + + public void Merge(List samples) + { + // TODO(beorn7): This tries to merge not only individual samples, but + // whole summaries. The paper doesn't mention merging summaries at + // all. Unittests show that the merging is inaccurate. Find out how to + // do merges properly. + + double r = 0; + var i = 0; + + for (var sampleIdx = 0; sampleIdx < samples.Count; sampleIdx++) + { + var sample = samples[sampleIdx]; + + for (; i < _samples.Count; i++) + { + var c = _samples[i]; + + if (c.Value > sample.Value) + { + // Insert at position i + _samples.Insert(i, new Sample { Value = sample.Value, Width = sample.Width, Delta = Math.Max(sample.Delta, Math.Floor(_invariant(this, r)) - 1) }); + i++; + goto inserted; + } + r += c.Width; + } + _samples.Add(new Sample { Value = sample.Value, Width = sample.Width, Delta = 0 }); + i++; + + inserted: + N += sample.Width; + r += sample.Width; + } + + Compress(); + } + + private void Compress() + { + if (_samples.Count < 2) + return; + + var x = _samples[_samples.Count - 1]; + var xi = _samples.Count - 1; + var r = N - 1 - x.Width; + + for (var i = _samples.Count - 2; i >= 0; i--) + { + var c = _samples[i]; + + if (c.Width + x.Width + x.Delta <= _invariant(this, r)) + { + x.Width += c.Width; + _samples[xi] = x; + _samples.RemoveAt(i); + xi -= 1; + } + else + { + x = c; + xi = i; + } + + r -= c.Width; + } + } + + public void Reset() + { + _samples.Clear(); + N = 0; + } + + public int Count => (int)N; + + public double Query(double q) + { + var t = Math.Ceiling(q * N); + t += Math.Ceiling(_invariant(this, t) / 2); + var p = _samples[0]; + double r = 0; + + for (var i = 1; i < _samples.Count; i++) + { + var c = _samples[i]; + r += p.Width; + + if (r + c.Width + c.Delta > t) + return p.Value; + + p = c; + } + + return p.Value; + } + + public int SampleCount => _samples.Count; +} \ No newline at end of file diff --git a/Prometheus/SuppressDefaultMetricOptions.cs b/Prometheus/SuppressDefaultMetricOptions.cs new file mode 100644 index 00000000..a78af281 --- /dev/null +++ b/Prometheus/SuppressDefaultMetricOptions.cs @@ -0,0 +1,95 @@ +namespace Prometheus; + +public sealed class SuppressDefaultMetricOptions +{ + internal static readonly SuppressDefaultMetricOptions SuppressAll = new() + { + SuppressProcessMetrics = true, + SuppressDebugMetrics = true, +#if NET + SuppressEventCounters = true, +#endif + +#if NET6_0_OR_GREATER + SuppressMeters = true +#endif + }; + + internal static readonly SuppressDefaultMetricOptions SuppressNone = new() + { + SuppressProcessMetrics = false, + SuppressDebugMetrics = false, +#if NET + SuppressEventCounters = false, +#endif + +#if NET6_0_OR_GREATER + SuppressMeters = false +#endif + }; + + /// + /// Suppress the current-process-inspecting metrics (uptime, resource use, ...). + /// + public bool SuppressProcessMetrics { get; set; } + + /// + /// Suppress metrics that prometheus-net uses to report debug information about itself (e.g. number of metrics exported). + /// + public bool SuppressDebugMetrics { get; set; } + +#if NET + /// + /// Suppress the default .NET Event Counter integration. + /// + public bool SuppressEventCounters { get; set; } +#endif + +#if NET6_0_OR_GREATER + /// + /// Suppress the .NET Meter API integration. + /// + public bool SuppressMeters { get; set; } +#endif + + internal sealed class ConfigurationCallbacks + { +#if NET + public Action ConfigureEventCounterAdapter = delegate { }; +#endif + +#if NET6_0_OR_GREATER + public Action ConfigureMeterAdapter = delegate { }; +#endif + } + + /// + /// Configures the default metrics registry based on the requested defaults behavior. + /// + internal void ApplyToDefaultRegistry(ConfigurationCallbacks configurationCallbacks) + { + if (!SuppressProcessMetrics) + DotNetStats.RegisterDefault(); + + if (!SuppressDebugMetrics) + Metrics.DefaultRegistry.StartCollectingRegistryMetrics(); + +#if NET + if (!SuppressEventCounters) + { + var options = new EventCounterAdapterOptions(); + configurationCallbacks.ConfigureEventCounterAdapter(options); + EventCounterAdapter.StartListening(options); + } +#endif + +#if NET6_0_OR_GREATER + if (!SuppressMeters) + { + var options = new MeterAdapterOptions(); + configurationCallbacks.ConfigureMeterAdapter(options); + MeterAdapter.StartListening(options); + } +#endif + } +} diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs new file mode 100644 index 00000000..f93fee80 --- /dev/null +++ b/Prometheus/TextSerializer.Net.cs @@ -0,0 +1,619 @@ +#if NET +using System; +using System.Buffers; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Prometheus; + +/// +/// Does NOT take ownership of the stream - caller remains the boss. +/// +internal sealed class TextSerializer : IMetricsSerializer +{ + internal static ReadOnlySpan NewLine => [(byte)'\n']; + internal static ReadOnlySpan Quote => [(byte)'"']; + internal static ReadOnlySpan Equal => [(byte)'=']; + internal static ReadOnlySpan Comma => [(byte)',']; + internal static ReadOnlySpan Underscore => [(byte)'_']; + internal static ReadOnlySpan LeftBrace => [(byte)'{']; + internal static ReadOnlySpan RightBraceSpace => [(byte)'}', (byte)' ']; + internal static ReadOnlySpan Space => [(byte)' ']; + internal static ReadOnlySpan SpaceHashSpaceLeftBrace => [(byte)' ', (byte)'#', (byte)' ', (byte)'{']; + internal static ReadOnlySpan PositiveInfinity => [(byte)'+', (byte)'I', (byte)'n', (byte)'f']; + internal static ReadOnlySpan NegativeInfinity => [(byte)'-', (byte)'I', (byte)'n', (byte)'f']; + internal static ReadOnlySpan NotANumber => [(byte)'N', (byte)'a', (byte)'N']; + internal static ReadOnlySpan DotZero => [(byte)'.', (byte)'0']; + internal static ReadOnlySpan FloatPositiveOne => [(byte)'1', (byte)'.', (byte)'0']; + internal static ReadOnlySpan FloatZero => [(byte)'0', (byte)'.', (byte)'0']; + internal static ReadOnlySpan FloatNegativeOne => [(byte)'-', (byte)'1', (byte)'.', (byte)'0']; + internal static ReadOnlySpan IntPositiveOne => [(byte)'1']; + internal static ReadOnlySpan IntZero => [(byte)'0']; + internal static ReadOnlySpan IntNegativeOne => [(byte)'-', (byte)'1']; + internal static ReadOnlySpan HashHelpSpace => [(byte)'#', (byte)' ', (byte)'H', (byte)'E', (byte)'L', (byte)'P', (byte)' ']; + internal static ReadOnlySpan NewlineHashTypeSpace => [(byte)'\n', (byte)'#', (byte)' ', (byte)'T', (byte)'Y', (byte)'P', (byte)'E', (byte)' ']; + + internal static readonly byte[] UnknownBytes = "unknown"u8.ToArray(); + internal static readonly byte[] EofNewLineBytes = [(byte)'#', (byte)' ', (byte)'E', (byte)'O', (byte)'F', (byte)'\n']; + internal static readonly byte[] PositiveInfinityBytes = [(byte)'+', (byte)'I', (byte)'n', (byte)'f']; + + internal static readonly Dictionary MetricTypeToBytes = new() + { + { MetricType.Gauge, "gauge"u8.ToArray() }, + { MetricType.Counter, "counter"u8.ToArray() }, + { MetricType.Histogram, "histogram"u8.ToArray() }, + { MetricType.Summary, "summary"u8.ToArray() }, + }; + + private static readonly char[] DotEChar = ['.', 'e']; + + public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) + { + _expositionFormat = fmt; + _stream = new Lazy(() => AddStreamBuffering(stream)); + } + + // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. + public TextSerializer(Func streamFactory, + ExpositionFormat fmt = ExpositionFormat.PrometheusText) + { + _expositionFormat = fmt; + _stream = new Lazy(() => AddStreamBuffering(streamFactory())); + } + + /// + /// Ensures that writes to the stream are buffered, meaning we do not emit individual "write 1 byte" calls to the stream. + /// This has been rumored by some users to be relevant in their scenarios (though never with solid evidence or repro steps). + /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing + /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform. + /// + private static Stream AddStreamBuffering(Stream inner) + { + return new BufferedStream(inner, bufferSize: 16 * 1024); + } + + public async Task FlushAsync(CancellationToken cancel) + { + // If we never opened the stream, we don't touch it on flush. + if (!_stream.IsValueCreated) + return; + + await _stream.Value.FlushAsync(cancel); + } + + private readonly Lazy _stream; + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel) + { + var bufferLength = MeasureFamilyDeclarationLength(name, nameBytes, helpBytes, type, typeBytes); + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var nameLen = nameBytes.Length; + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + { + if (name.EndsWith("_total")) + { + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. + } + else + { + typeBytes = UnknownBytes; // if the total prefix is missing the _total prefix it is out of spec + } + } + + var position = 0; + AppendToBufferAndIncrementPosition(HashHelpSpace, buffer, ref position); + AppendToBufferAndIncrementPosition(nameBytes.AsSpan(0, nameLen), buffer, ref position); + // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + if (helpBytes.Length > 0) + { + AppendToBufferAndIncrementPosition(helpBytes, buffer, ref position); + } + AppendToBufferAndIncrementPosition(NewlineHashTypeSpace, buffer, ref position); + AppendToBufferAndIncrementPosition(nameBytes.AsSpan(0, nameLen), buffer, ref position); + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + AppendToBufferAndIncrementPosition(typeBytes, buffer, ref position); + AppendToBufferAndIncrementPosition(NewLine, buffer, ref position); + + await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public int MeasureFamilyDeclarationLength(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + var nameLen = nameBytes.Length; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + { + if (name.EndsWith("_total")) + { + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. + } + else + { + typeBytes = UnknownBytes; // if the total prefix is missing the _total prefix it is out of spec + } + } + + length += HashHelpSpace.Length; + length += nameLen; + // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. + length += Space.Length; + length += helpBytes.Length; + length += NewlineHashTypeSpace.Length; + length += nameLen; + length += Space.Length; + length += typeBytes.Length; + length += NewLine.Length; + + return length; + } + + public async ValueTask WriteEnd(CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync(EofNewLineBytes, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) + { + // This is a max length because we do not know ahead of time how many bytes the actual value will consume. + var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + bufferMaxLength += MeasureExemplarMaxLength(exemplar); + + var buffer = ArrayPool.Shared.Rent(bufferMaxLength); + + try + { + var position = WriteIdentifierPart(buffer, name, flattenedLabels, canonicalLabel, suffix); + + position += WriteValue(buffer.AsSpan(position..), value); + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + position += WriteExemplar(buffer.AsSpan(position..), exemplar); + } + + AppendToBufferAndIncrementPosition(NewLine, buffer, ref position); + + ValidateBufferMaxLengthAndPosition(bufferMaxLength, position); + + await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) + { + // This is a max length because we do not know ahead of time how many bytes the actual value will consume. + var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + bufferMaxLength += MeasureExemplarMaxLength(exemplar); + + var buffer = ArrayPool.Shared.Rent(bufferMaxLength); + + try + { + var position = WriteIdentifierPart(buffer, name, flattenedLabels, canonicalLabel, suffix); + + position += WriteValue(buffer.AsSpan(position..), value); + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + position += WriteExemplar(buffer.AsSpan(position..), exemplar); + } + + AppendToBufferAndIncrementPosition(NewLine, buffer, ref position); + + ValidateBufferMaxLengthAndPosition(bufferMaxLength, position); + + await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private int WriteExemplar(Span buffer, ObservedExemplar exemplar) + { + var position = 0; + + AppendToBufferAndIncrementPosition(SpaceHashSpaceLeftBrace, buffer, ref position); + + for (var i = 0; i < exemplar.Labels!.Length; i++) + { + if (i > 0) + AppendToBufferAndIncrementPosition(Comma, buffer, ref position); + + ref var labelPair = ref exemplar.Labels[i]; + position += WriteExemplarLabel(buffer[position..], labelPair.KeyBytes, labelPair.Value); + } + + AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); + position += WriteValue(buffer[position..], exemplar.Value); + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + position += WriteValue(buffer[position..], exemplar.Timestamp); + + return position; + } + + private int MeasureExemplarMaxLength(ObservedExemplar exemplar) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + length += SpaceHashSpaceLeftBrace.Length; + + for (var i = 0; i < exemplar.Labels!.Length; i++) + { + if (i > 0) + length += Comma.Length; + + ref var labelPair = ref exemplar.Labels[i]; + length += MeasureExemplarLabelMaxLength(labelPair.KeyBytes, labelPair.Value); + } + + length += RightBraceSpace.Length; + length += MeasureValueMaxLength(exemplar.Value); + length += Space.Length; + length += MeasureValueMaxLength(exemplar.Timestamp); + + return length; + } + + private static int WriteExemplarLabel(Span buffer, byte[] label, string value) + { + var position = 0; + + AppendToBufferAndIncrementPosition(label, buffer, ref position); + AppendToBufferAndIncrementPosition(Equal, buffer, ref position); + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + position += PrometheusConstants.ExemplarEncoding.GetBytes(value.AsSpan(), buffer[position..]); + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + + return position; + } + + private static int MeasureExemplarLabelMaxLength(byte[] label, string value) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + length += label.Length; + length += Equal.Length; + length += Quote.Length; + length += PrometheusConstants.ExemplarEncoding.GetMaxByteCount(value.Length); + length += Quote.Length; + + return length; + } + + private int WriteValue(Span buffer, double value) + { + var position = 0; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + AppendToBufferAndIncrementPosition(FloatZero, buffer, ref position); + return position; + case 1: + AppendToBufferAndIncrementPosition(FloatPositiveOne, buffer, ref position); + return position; + case -1: + AppendToBufferAndIncrementPosition(FloatNegativeOne, buffer, ref position); + return position; + case double.PositiveInfinity: + AppendToBufferAndIncrementPosition(PositiveInfinity, buffer, ref position); + return position; + case double.NegativeInfinity: + AppendToBufferAndIncrementPosition(NegativeInfinity, buffer, ref position); + return position; + case double.NaN: + AppendToBufferAndIncrementPosition(NotANumber, buffer, ref position); + return position; + } + } + + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode floating point value as string."); + + var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); + AppendToBufferAndIncrementPosition(_stringBytesBuffer.AsSpan(0, encodedBytes), buffer, ref position); + + // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. + if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresOpenMetricsDotZero(_stringCharsBuffer, charsWritten)) + AppendToBufferAndIncrementPosition(DotZero, buffer, ref position); + + return position; + } + + static bool RequiresOpenMetricsDotZero(char[] buffer, int length) + { + return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e, so needs a .0 to turn it into a floating-point value */ + } + + private int MeasureValueMaxLength(double value) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + return FloatZero.Length; + case 1: + return FloatPositiveOne.Length; + case -1: + return FloatNegativeOne.Length; + case double.PositiveInfinity: + return PositiveInfinity.Length; + case double.NegativeInfinity: + return NegativeInfinity.Length; + case double.NaN: + return NotANumber.Length; + } + } + + // We do not want to spend time formatting the value just to measure the length and throw away the result. + // Therefore we just consider the max length and return it. The max length is just the length of the value-encoding buffer. + return _stringBytesBuffer.Length; + } + + private int WriteValue(Span buffer, long value) + { + var position = 0; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + AppendToBufferAndIncrementPosition(IntZero, buffer, ref position); + return position; + case 1: + AppendToBufferAndIncrementPosition(IntPositiveOne, buffer, ref position); + return position; + case -1: + AppendToBufferAndIncrementPosition(IntNegativeOne, buffer, ref position); + return position; + } + } + + if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "D", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode integer value as string."); + + var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); + AppendToBufferAndIncrementPosition(_stringBytesBuffer.AsSpan(0, encodedBytes), buffer, ref position); + + return position; + } + + private int MeasureValueMaxLength(long value) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + return IntZero.Length; + case 1: + return IntPositiveOne.Length; + case -1: + return IntNegativeOne.Length; + } + } + + // We do not want to spend time formatting the value just to measure the length and throw away the result. + // Therefore we just consider the max length and return it. The max length is just the length of the value-encoding buffer. + return _stringBytesBuffer.Length; + } + + // Reuse a buffer to do the serialization and UTF-8 encoding. + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + private readonly char[] _stringCharsBuffer = new char[32]; + private readonly byte[] _stringBytesBuffer = new byte[32]; + + private readonly ExpositionFormat _expositionFormat; + + private static void AppendToBufferAndIncrementPosition(ReadOnlySpan from, Span to, ref int position) + { + from.CopyTo(to[position..]); + position += from.Length; + } + + private static void ValidateBufferLengthAndPosition(int bufferLength, int position) + { + if (position != bufferLength) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + } + + private static void ValidateBufferMaxLengthAndPosition(int bufferMaxLength, int position) + { + if (position > bufferMaxLength) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + } + + /// + /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. + /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} + /// Note: Terminates with a SPACE + /// + private int WriteIdentifierPart(Span buffer, byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, byte[]? suffix = null) + { + var position = 0; + + AppendToBufferAndIncrementPosition(name, buffer, ref position); + + if (suffix != null && suffix.Length > 0) + { + AppendToBufferAndIncrementPosition(Underscore, buffer, ref position); + AppendToBufferAndIncrementPosition(suffix, buffer, ref position); + } + + if (flattenedLabels.Length > 0 || extraLabel.IsNotEmpty) + { + AppendToBufferAndIncrementPosition(LeftBrace, buffer, ref position); + if (flattenedLabels.Length > 0) + { + AppendToBufferAndIncrementPosition(flattenedLabels, buffer, ref position); + } + + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (extraLabel.IsNotEmpty) + { + if (flattenedLabels.Length > 0) + { + AppendToBufferAndIncrementPosition(Comma, buffer, ref position); + } + + AppendToBufferAndIncrementPosition(extraLabel.Name, buffer, ref position); + AppendToBufferAndIncrementPosition(Equal, buffer, ref position); + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + AppendToBufferAndIncrementPosition(extraLabel.OpenMetrics, buffer, ref position); + else + AppendToBufferAndIncrementPosition(extraLabel.Prometheus, buffer, ref position); + + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + } + + AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); + } + else + { + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + } + + return position; + } + + private int MeasureIdentifierPartLength(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, byte[]? suffix = null) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + length += name.Length; + + if (suffix != null && suffix.Length > 0) + { + length += Underscore.Length; + length += suffix.Length; + } + + if (flattenedLabels.Length > 0 || extraLabel.IsNotEmpty) + { + length += LeftBrace.Length; + if (flattenedLabels.Length > 0) + { + length += flattenedLabels.Length; + } + + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (extraLabel.IsNotEmpty) + { + if (flattenedLabels.Length > 0) + { + length += Comma.Length; + } + + length += extraLabel.Name.Length; + length += Equal.Length; + length += Quote.Length; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + length += extraLabel.OpenMetrics.Length; + else + length += extraLabel.Prometheus.Length; + + length += Quote.Length; + } + + length += RightBraceSpace.Length; + } + else + { + length += Space.Length; + } + + return length; + } + + /// + /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be + /// the same. + /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers + /// + internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) + { + if (double.IsPositiveInfinity(value)) + return new CanonicalLabel(name, PositiveInfinityBytes, PositiveInfinityBytes); + + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + Span buffer = stackalloc char[32]; + + if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode floating point value as string."); + + var prometheusChars = buffer[0..charsWritten]; + + var prometheusByteCount = PrometheusConstants.ExportEncoding.GetByteCount(prometheusChars); + var prometheusBytes = new byte[prometheusByteCount]; + + if (PrometheusConstants.ExportEncoding.GetBytes(prometheusChars, prometheusBytes) != prometheusByteCount) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + + var openMetricsByteCount = prometheusByteCount; + byte[] openMetricsBytes; + + // Identify whether the written characters are expressed as floating-point, by checking for presence of the 'e' or '.' characters. + if (prometheusChars.IndexOfAny(DotEChar) == -1) + { + // Prometheus defaults to integer-formatting without a decimal point, if possible. + // OpenMetrics requires labels containing numeric values to be expressed in floating point format. + // If all we find is an integer, we add a ".0" to the end to make it a floating point value. + openMetricsByteCount += 2; + + openMetricsBytes = new byte[openMetricsByteCount]; + Array.Copy(prometheusBytes, openMetricsBytes, prometheusByteCount); + + DotZero.CopyTo(openMetricsBytes.AsSpan(prometheusByteCount)); + } + else + { + // It is already a floating-point value in Prometheus representation - reuse same bytes for OpenMetrics. + openMetricsBytes = prometheusBytes; + } + + return new CanonicalLabel(name, prometheusBytes, openMetricsBytes); + } +} +#endif \ No newline at end of file diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs new file mode 100644 index 00000000..9b012c21 --- /dev/null +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -0,0 +1,317 @@ +#if !NET +using System.Globalization; + +namespace Prometheus; + +/// +/// Does NOT take ownership of the stream - caller remains the boss. +/// +internal sealed class TextSerializer : IMetricsSerializer +{ + internal static readonly byte[] NewLine = [(byte)'\n']; + internal static readonly byte[] Quote = [(byte)'"']; + internal static readonly byte[] Equal = [(byte)'=']; + internal static readonly byte[] Comma = [(byte)',']; + internal static readonly byte[] Underscore = [(byte)'_']; + internal static readonly byte[] LeftBrace = [(byte)'{']; + internal static readonly byte[] RightBraceSpace = [(byte)'}', (byte)' ']; + internal static readonly byte[] Space = [(byte)' ']; + internal static readonly byte[] SpaceHashSpaceLeftBrace = [(byte)' ', (byte)'#', (byte)' ', (byte)'{']; + internal static readonly byte[] PositiveInfinity = "+Inf"u8.ToArray(); + internal static readonly byte[] NegativeInfinity = "-Inf"u8.ToArray(); + internal static readonly byte[] NotANumber = "NaN"u8.ToArray(); + internal static readonly byte[] DotZero = ".0"u8.ToArray(); + internal static readonly byte[] FloatPositiveOne = "1.0"u8.ToArray(); + internal static readonly byte[] FloatZero = "0.0"u8.ToArray(); + internal static readonly byte[] FloatNegativeOne = "-1.0"u8.ToArray(); + internal static readonly byte[] IntPositiveOne = "1"u8.ToArray(); + internal static readonly byte[] IntZero = "0"u8.ToArray(); + internal static readonly byte[] IntNegativeOne = "-1"u8.ToArray(); + internal static readonly byte[] EofNewLine = "# EOF\n"u8.ToArray(); + internal static readonly byte[] HashHelpSpace = "# HELP "u8.ToArray(); + internal static readonly byte[] NewlineHashTypeSpace = "\n# TYPE "u8.ToArray(); + + internal static readonly byte[] Unknown = "unknown"u8.ToArray(); + + internal static readonly Dictionary MetricTypeToBytes = new() + { + { MetricType.Gauge, "gauge"u8.ToArray() }, + { MetricType.Counter, "counter"u8.ToArray() }, + { MetricType.Histogram, "histogram"u8.ToArray() }, + { MetricType.Summary, "summary"u8.ToArray() }, + }; + + private static readonly char[] DotEChar = ['.', 'e']; + + public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) + { + _expositionFormat = fmt; + _stream = new Lazy(() => AddStreamBuffering(stream)); + } + + // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. + public TextSerializer(Func streamFactory, + ExpositionFormat fmt = ExpositionFormat.PrometheusText) + { + _expositionFormat = fmt; + _stream = new Lazy(() => AddStreamBuffering(streamFactory())); + } + + /// + /// Ensures that writes to the stream are buffered, meaning we do not emit individual "write 1 byte" calls to the stream. + /// This has been rumored by some users to be relevant in their scenarios (though never with solid evidence or repro steps). + /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing + /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform. + /// + private static Stream AddStreamBuffering(Stream inner) + { + return new BufferedStream(inner); + } + + public async Task FlushAsync(CancellationToken cancel) + { + // If we never opened the stream, we don't touch it on flush. + if (!_stream.IsValueCreated) + return; + + await _stream.Value.FlushAsync(cancel); + } + + private readonly Lazy _stream; + + public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel) + { + var nameLen = nameBytes.Length; + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + { + if (name.EndsWith("_total")) + { + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. + } + else + { + typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec + } + } + + await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); + await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + if (helpBytes.Length > 0) + { + await _stream.Value.WriteAsync(helpBytes, 0, helpBytes.Length, cancel); + } + await _stream.Value.WriteAsync(NewlineHashTypeSpace, 0, NewlineHashTypeSpace.Length, cancel); + await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await _stream.Value.WriteAsync(typeBytes, 0, typeBytes.Length, cancel); + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } + + public async ValueTask WriteEnd(CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel); + } + + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) + { + await WriteIdentifierPartAsync(name, flattenedLabels, canonicalLabel, suffix, cancel); + + await WriteValue(value, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } + + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } + + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) + { + await WriteIdentifierPartAsync(name, flattenedLabels, canonicalLabel, suffix, cancel); + + await WriteValue(value, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } + + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } + + private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) + { + await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel); + for (var i = 0; i < exemplar.Labels!.Length; i++) + { + if (i > 0) + await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); + + await WriteLabel(exemplar.Labels![i].KeyBytes, PrometheusConstants.ExemplarEncoding.GetBytes(exemplar.Labels![i].Value), cancel); + } + + await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); + await WriteValue(exemplar.Value, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await WriteValue(exemplar.Timestamp, cancel); + } + + private async Task WriteLabel(byte[] label, byte[] value, CancellationToken cancel) + { + await _stream.Value.WriteAsync(label, 0, label.Length, cancel); + await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + await _stream.Value.WriteAsync(value, 0, value.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + } + + private async Task WriteValue(double value, CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + await _stream.Value.WriteAsync(FloatZero, 0, FloatZero.Length, cancel); + return; + case 1: + await _stream.Value.WriteAsync(FloatPositiveOne, 0, FloatPositiveOne.Length, cancel); + return; + case -1: + await _stream.Value.WriteAsync(FloatNegativeOne, 0, FloatNegativeOne.Length, cancel); + return; + case double.PositiveInfinity: + await _stream.Value.WriteAsync(PositiveInfinity, 0, PositiveInfinity.Length, cancel); + return; + case double.NegativeInfinity: + await _stream.Value.WriteAsync(NegativeInfinity, 0, NegativeInfinity.Length, cancel); + return; + case double.NaN: + await _stream.Value.WriteAsync(NotANumber, 0, NotANumber.Length, cancel); + return; + } + } + + var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); + + var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); + + // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. + if (_expositionFormat == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */) + await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel); + } + + private async Task WriteValue(long value, CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + await _stream.Value.WriteAsync(IntZero, 0, IntZero.Length, cancel); + return; + case 1: + await _stream.Value.WriteAsync(IntPositiveOne, 0, IntPositiveOne.Length, cancel); + return; + case -1: + await _stream.Value.WriteAsync(IntNegativeOne, 0, IntNegativeOne.Length, cancel); + return; + } + } + + var valueAsString = value.ToString("D", CultureInfo.InvariantCulture); + var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); + } + + // Reuse a buffer to do the serialization and UTF-8 encoding. + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + private readonly char[] _stringCharsBuffer = new char[32]; + private readonly byte[] _stringBytesBuffer = new byte[32]; + + private readonly ExpositionFormat _expositionFormat; + + /// + /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. + /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} + /// Note: Terminates with a SPACE + /// + private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, + CanonicalLabel canonicalLabel, byte[]? suffix, CancellationToken cancel) + { + await _stream.Value.WriteAsync(name, 0, name.Length, cancel); + if (suffix != null && suffix.Length > 0) + { + await _stream.Value.WriteAsync(Underscore, 0, Underscore.Length, cancel); + await _stream.Value.WriteAsync(suffix, 0, suffix.Length, cancel); + } + + if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty) + { + await _stream.Value.WriteAsync(LeftBrace, 0, LeftBrace.Length, cancel); + if (flattenedLabels.Length > 0) + { + await _stream.Value.WriteAsync(flattenedLabels, 0, flattenedLabels.Length, cancel); + } + + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (canonicalLabel.IsNotEmpty) + { + if (flattenedLabels.Length > 0) + { + await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); + } + + await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel); + await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync( + canonicalLabel.OpenMetrics, 0, canonicalLabel.OpenMetrics.Length, cancel); + else + await _stream.Value.WriteAsync( + canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + } + + await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); + } + else + { + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + } + } + + /// + /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be + /// the same. + /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers + /// + internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) + { + if (double.IsPositiveInfinity(value)) + return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); + + var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); + var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); + + var openMetricsBytes = prometheusBytes; + + // Identify whether the original value is floating-point, by checking for presence of the 'e' or '.' characters. + if (valueAsString.IndexOfAny(DotEChar) == -1) + { + // OpenMetrics requires labels containing numeric values to be expressed in floating point format. + // If all we find is an integer, we add a ".0" to the end to make it a floating point value. + openMetricsBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0"); + } + + return new CanonicalLabel(name, prometheusBytes, openMetricsBytes); + } +} +#endif \ No newline at end of file diff --git a/Prometheus/ThreadSafeDouble.cs b/Prometheus/ThreadSafeDouble.cs new file mode 100644 index 00000000..5f24e99b --- /dev/null +++ b/Prometheus/ThreadSafeDouble.cs @@ -0,0 +1,91 @@ +using System.Globalization; + +namespace Prometheus; + +internal struct ThreadSafeDouble +{ + private long _value; + + public ThreadSafeDouble(double value) + { + _value = BitConverter.DoubleToInt64Bits(value); + } + + public double Value + { + get + { + return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _value)); + } + set + { + Interlocked.Exchange(ref _value, BitConverter.DoubleToInt64Bits(value)); + } + } + + public void Add(double increment) + { + while (true) + { + long initialValue = Volatile.Read(ref _value); + double computedValue = BitConverter.Int64BitsToDouble(initialValue) + increment; + + if (initialValue == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(computedValue), initialValue)) + return; + } + } + + /// + /// Sets the value to this, unless the existing value is already greater. + /// + public void IncrementTo(double to) + { + while (true) + { + long initialRaw = Volatile.Read(ref _value); + double initialValue = BitConverter.Int64BitsToDouble(initialRaw); + + if (initialValue >= to) + return; // Already greater. + + if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) + return; + } + } + + /// + /// Sets the value to this, unless the existing value is already smaller. + /// + public void DecrementTo(double to) + { + while (true) + { + long initialRaw = Volatile.Read(ref _value); + double initialValue = BitConverter.Int64BitsToDouble(initialRaw); + + if (initialValue <= to) + return; // Already smaller. + + if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) + return; + } + } + + public override string ToString() + { + return Value.ToString(CultureInfo.InvariantCulture); + } + + public override bool Equals(object? obj) + { + if (obj is ThreadSafeDouble other) + return Value.Equals(other.Value); + + return Value.Equals(obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } +} diff --git a/Prometheus/ThreadSafeLong.cs b/Prometheus/ThreadSafeLong.cs new file mode 100644 index 00000000..a81f7aab --- /dev/null +++ b/Prometheus/ThreadSafeLong.cs @@ -0,0 +1,48 @@ +using System.Globalization; + +namespace Prometheus; + +internal struct ThreadSafeLong +{ + private long _value; + + public ThreadSafeLong(long value) + { + _value = value; + } + + public long Value + { + get + { + return Interlocked.Read(ref _value); + } + set + { + Interlocked.Exchange(ref _value, value); + } + } + + public void Add(long increment) + { + Interlocked.Add(ref _value, increment); + } + + public override string ToString() + { + return Value.ToString(CultureInfo.InvariantCulture); + } + + public override bool Equals(object? obj) + { + if (obj is ThreadSafeLong other) + return Value.Equals(other.Value); + + return Value.Equals(obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } +} diff --git a/Prometheus/TimerExtensions.cs b/Prometheus/TimerExtensions.cs new file mode 100644 index 00000000..b1f3bcf3 --- /dev/null +++ b/Prometheus/TimerExtensions.cs @@ -0,0 +1,66 @@ +namespace Prometheus; + +public static class TimerExtensions +{ + private sealed class Timer : ITimer + { + private readonly ValueStopwatch _stopwatch = ValueStopwatch.StartNew(); + private readonly Action _observeDurationAction; + + public Timer(IObserver observer) + { + _observeDurationAction = observer.Observe; + } + + public Timer(IGauge gauge) + { + _observeDurationAction = gauge.Set; + } + + public Timer(ICounter counter) + { + _observeDurationAction = counter.Inc; + } + + public TimeSpan ObserveDuration() + { + var duration = _stopwatch.GetElapsedTime(); + _observeDurationAction(duration.TotalSeconds); + + return duration; + } + + public void Dispose() + { + ObserveDuration(); + } + } + + /// + /// Enables you to easily report elapsed seconds in the value of an observer. + /// Dispose of the returned instance to report the elapsed duration. + /// + public static ITimer NewTimer(this IObserver observer) + { + return new Timer(observer); + } + + /// + /// Enables you to easily report elapsed seconds in the value of a gauge. + /// Dispose of the returned instance to report the elapsed duration. + /// + public static ITimer NewTimer(this IGauge gauge) + { + return new Timer(gauge); + } + + /// + /// Enables you to easily report elapsed seconds in the value of a counter. + /// The duration (in seconds) will be added to the value of the counter. + /// Dispose of the returned instance to report the elapsed duration. + /// + public static ITimer NewTimer(this ICounter counter) + { + return new Timer(counter); + } +} diff --git a/Prometheus/TimestampHelpers.cs b/Prometheus/TimestampHelpers.cs new file mode 100644 index 00000000..c47ce850 --- /dev/null +++ b/Prometheus/TimestampHelpers.cs @@ -0,0 +1,25 @@ +namespace Prometheus; + +static class TimestampHelpers +{ + // Math copypasted from DateTimeOffset.cs in .NET Framework. + + // Number of days in a non-leap year + private const int DaysPerYear = 365; + // Number of days in 4 years + private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 + // Number of days in 100 years + private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 + // Number of days in 400 years + private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 + private const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162 + private const long UnixEpochTicks = TimeSpan.TicksPerDay * DaysTo1970; // 621,355,968,000,000,000 + private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 + + public static double ToUnixTimeSecondsAsDouble(DateTimeOffset timestamp) + { + // This gets us sub-millisecond precision, which is better than ToUnixTimeMilliseconds(). + var ticksSinceUnixEpoch = timestamp.ToUniversalTime().Ticks - UnixEpochSeconds * TimeSpan.TicksPerSecond; + return ticksSinceUnixEpoch / (double)TimeSpan.TicksPerSecond; + } +} diff --git a/Prometheus/Usings.cs b/Prometheus/Usings.cs new file mode 100644 index 00000000..e6a9eb40 --- /dev/null +++ b/Prometheus/Usings.cs @@ -0,0 +1 @@ +global using System.Net.Http; \ No newline at end of file diff --git a/Prometheus/ValueStopwatch.cs b/Prometheus/ValueStopwatch.cs new file mode 100644 index 00000000..87aa33d0 --- /dev/null +++ b/Prometheus/ValueStopwatch.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; + +// Copied from: https://github.com/dotnet/extensions/blob/master/src/Shared/src/ValueStopwatch/ValueStopwatch.cs +namespace Prometheus; + +internal readonly struct ValueStopwatch +{ + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + + private readonly long _startTimestamp; + + private ValueStopwatch(long startTimestamp) + { + _startTimestamp = startTimestamp; + } + + public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp()); + + public TimeSpan GetElapsedTime() + { + // Start timestamp can't be zero in an initialized ValueStopwatch. + // It would have to be literally the first thing executed when the machine boots to be 0. + // So it being 0 is a clear indication of default(ValueStopwatch) + if (_startTimestamp == 0) + throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); + + var end = Stopwatch.GetTimestamp(); + var timestampDelta = end - _startTimestamp; + var ticks = (long)(TimestampToTicks * timestampDelta); + return new TimeSpan(ticks); + } +} diff --git a/README.md b/README.md index d6566a4b..9ddfd113 100644 --- a/README.md +++ b/README.md @@ -6,49 +6,35 @@ This is a .NET library for instrumenting your applications and exporting metrics ![](Screenshot.png) -The library targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) which supports the following runtimes (and newer): - -* .NET Framework 4.6.1 -* .NET Core 2.0 -* Mono 5.4 - -Some specialized subsets of functionality require more modern runtimes: - -* The ASP.NET Core specific functionality requires ASP.NET Core 2.1 or newer. -* The .NET Core specific functionality requires .NET Core 2.1 or newer. -* The gRPC specific functionality requires .NET Core 3.1 or newer. - -Related projects: - -* [prometheus-net.DotNetRuntime](https://github.com/djluck/prometheus-net.DotNetRuntime) instruments .NET Core apps to export metrics on .NET Core performance. -* [prometheus-net.AspNet](https://github.com/rocklan/prometheus-net.AspNet) instruments ASP.NET full framework apps to export metrics on performance. -* [prometheus-net.SystemMetrics](https://github.com/Daniel15/prometheus-net.SystemMetrics) exports various system metrics such as CPU usage, disk usage, etc. -* [prometheus-net/docker_exporter](https://github.com/prometheus-net/docker_exporter) exports metrics about a Docker installation. -* [prometheus-net/tzsp_packetstream_exporter](https://github.com/prometheus-net/tzsp_packetstream_exporter) exports metrics about the data flows found in a stream of IPv4 packets. -* [prometheus-net Grafana dashboards](https://github.com/prometheus-net/grafana-dashboards) provides example dashboards for visualizing prometheus-net metrics in [Grafana](https://grafana.com/). +The library targets the following runtimes (and newer): +* .NET Framework 4.6.2 +* .NET 6.0 # Table of contents * [Best practices and usage](#best-practices-and-usage) -* [Installation](#installation) * [Quick start](#quick-start) +* [Installation](#installation) * [Counters](#counters) * [Gauges](#gauges) -* [Summary](#summary) * [Histogram](#histogram) +* [Summary](#summary) * [Measuring operation duration](#measuring-operation-duration) * [Tracking in-progress operations](#tracking-in-progress-operations) * [Counting exceptions](#counting-exceptions) * [Labels](#labels) * [Static labels](#static-labels) +* [Exemplars](#exemplars) +* [Limiting exemplar volume](#limiting-exemplar-volume) * [When are metrics published?](#when-are-metrics-published) +* [Deleting metrics](#deleting-metrics) * [ASP.NET Core exporter middleware](#aspnet-core-exporter-middleware) * [ASP.NET Core HTTP request metrics](#aspnet-core-http-request-metrics) * [ASP.NET Core gRPC request metrics](#aspnet-core-grpc-request-metrics) * [IHttpClientFactory metrics](#ihttpclientfactory-metrics) * [ASP.NET Core health check status metrics](#aspnet-core-health-check-status-metrics) -* [ASP.NET Core with basic authentication](#aspnet-core-with-basic-authentication) +* [Protecting the metrics endpoint from unauthorized access](#protecting-the-metrics-endpoint-from-unauthorized-access) * [ASP.NET Web API exporter](#aspnet-web-api-exporter) * [Kestrel stand-alone server](#kestrel-stand-alone-server) * [Publishing to Pushgateway](#publishing-to-pushgateway) @@ -58,6 +44,10 @@ Related projects: * [Just-in-time updates](#just-in-time-updates) * [Suppressing default metrics](#suppressing-default-metrics) * [DiagnosticSource integration](#diagnosticsource-integration) +* [EventCounter integration](#eventcounter-integration) +* [.NET Meters integration](#net-meters-integration) +* [Benchmarks](#benchmarks) +* [Community projects](#community-projects) # Best practices and usage @@ -71,6 +61,44 @@ Four types of metrics are available: Counter, Gauge, Summary and Histogram. See More complex patterns may also be used (e.g. combining with dependency injection). The library is quite tolerant of different usage models - if the API allows it, it will generally work fine and provide satisfactory performance. The library is thread-safe. +# Quick start + +After installing the library, you should: + +1. Collect some metrics, either by using built-in integrations or publishing your own custom metrics. +1. Export the collected metrics over an HTTP endpoint (typically `/metrics`). +1. Configure a Prometheus server to poll this endpoint for metrics on a regular interval. + +Minimal sample app (based on .NET 6 Console app template): + +```csharp +using var server = new Prometheus.KestrelMetricServer(port: 1234); +server.Start(); + +Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); +Console.WriteLine("Press enter to exit."); +Console.ReadLine(); +``` + +Refer to the sample projects for quick start instructions: + +| Name | Description | +|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| [Sample.Web](Sample.Web/Program.cs) | ASP.NET Core application that produces custom metrics and uses multiple integrations to publish built-in metrics | +| [Sample.Console](Sample.Console/Program.cs) | .NET console application that exports custom metrics | +| [Sample.Console.DotNetMeters](Sample.Console.DotNetMeters/Program.cs) | Demonstrates how to [publish custom metrics via the .NET Meters API](#net-meters-integration) | +| [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs) | .NET console application that attaches exemplars to some metrics | +| [Sample.Console.NetFramework](Sample.Console.NetFramework/Program.cs) | Same as above but targeting .NET Framework | +| [Sample.Console.NoAspNetCore](Sample.Console.NoAspNetCore/Program.cs) | .NET console application that exports custom metrics without requiring the ASP.NET Core runtime to be installed | +| [Sample.Grpc](Sample.Grpc/Program.cs) | ASP.NET Core application that publishes a gRPC service | +| [Sample.Grpc.Client](Sample.Grpc.Client/Program.cs) | Client app for the above | +| [Sample.NetStandard](Sample.NetStandard/ImportantProcess.cs) | Demonstrates how to reference prometheus-net in a .NET Standard class library | +| [Sample.Web.DifferentPort](Sample.Web.DifferentPort/Program.cs) | Demonstrates how to set up the metric exporter on a different port from the main web API (e.g. for security purposes) | +| [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs) | Demonstrates how to use [automatic metric deletion](#deleting-metrics) | +| [Sample.Web.NetFramework](Sample.Web.NetFramework/Global.asax.cs) | .NET Framework web app that publishes custom metrics | + +The rest of this document describes how to use individual features of the library. + # Installation Nuget package for general use and metrics export via HttpListener or to Pushgateway: [prometheus-net](https://www.nuget.org/packages/prometheus-net) @@ -93,45 +121,6 @@ Nuget package for ASP.NET Web API middleware on .NET Framework: [prometheus-net. >Install-Package prometheus-net.NetFramework.AspNet - -# Quick start - -After installing the library, you should: - -1. Initialize some metrics and start updating their values. -1. Publish the collected metrics over HTTP. -1. Configure the Prometheus server to poll your app for metrics on regular intervals. - -The chapters below describe the various ways you can initialize or update metrics and the ways in which they can be published. - -The following is a minimal implementation that simply increments a counter once a second, publishing the metrics on http://localhost:1234/metrics - -```csharp -using Prometheus; -using System; -using System.Threading; - -class Program -{ - private static readonly Counter TickTock = - Metrics.CreateCounter("sampleapp_ticks_total", "Just keeps on ticking"); - - static void Main() - { - var server = new MetricServer(hostname: "localhost", port: 1234); - server.Start(); - - while (true) - { - TickTock.Inc(); - Thread.Sleep(TimeSpan.FromSeconds(1)); - } - } -} -``` - -**NB!** The quick start example only exposes metrics on the `http://localhost` URL. To access the metrics endpoint from other systems you need to remove the `hostname` argument and, on Windows, configure HTTP listener permissions. For more information, see [Publishing via standalone HTTP handler](#publishing-via-standalone-http-handler) for configuration instructions or consider using [ASP.NET Core exporter middleware](#aspnet-core-exporter-middleware) which requires no extra configuration. - # Counters Counters only increase in value and reset to zero when the process restarts. @@ -165,6 +154,24 @@ var job = jobQueue.Dequeue(); JobsInQueue.Dec(); ``` +# Histogram + +Histograms track the size and number of events in buckets. This allows for aggregatable calculation of quantiles. + +```csharp +private static readonly Histogram OrderValueHistogram = Metrics + .CreateHistogram("myapp_order_value_usd", "Histogram of received order values (in USD).", + new HistogramConfiguration + { + // We divide measurements in 10 buckets of $100 each, up to $1000. + Buckets = Histogram.LinearBuckets(start: 100, width: 100, count: 10) + }); + +... + +OrderValueHistogram.Observe(order.TotalValueUsd); +``` + # Summary Summaries track the trends in events over time (10 minutes by default). @@ -197,24 +204,6 @@ private static readonly Summary RequestSizeSummary = Metrics The epsilon indicates the absolute error allowed in measurements. For more information, refer to the [Prometheus documentation on summaries and histograms](https://prometheus.io/docs/practices/histograms/). -# Histogram - -Histograms track the size and number of events in buckets. This allows for aggregatable calculation of quantiles. - -```csharp -private static readonly Histogram OrderValueHistogram = Metrics - .CreateHistogram("myapp_order_value_usd", "Histogram of received order values (in USD).", - new HistogramConfiguration - { - // We divide measurements in 10 buckets of $100 each, up to $1000. - Buckets = Histogram.LinearBuckets(start: 100, width: 100, count: 10) - }); - -... - -OrderValueHistogram.Observe(order.TotalValueUsd); -``` - # Measuring operation duration Timers can be used to report the duration of an operation (in seconds) to a Summary, Histogram, Gauge or Counter. Wrap the operation you want to measure in a using block. @@ -287,17 +276,12 @@ Taking a counter as an example: ```csharp private static readonly Counter RequestCountByMethod = Metrics - .CreateCounter("myapp_requests_total", "Number of requests received, by HTTP method.", - new CounterConfiguration - { - // Here you specify only the names of the labels. - LabelNames = new[] { "method" } - }); + .CreateCounter("myapp_requests_total", "Number of requests received, by HTTP method.", labelNames: new[] { "method" }); ... // You can specify the values for the labels later, once you know the right values (e.g in your request handler code). -counter.WithLabels("GET").Inc(); +RequestCountByMethod.WithLabels("GET").Inc(); ``` NB! Best practices of metric design is to **minimize the number of different label values**. For example: @@ -310,9 +294,9 @@ NB! Best practices of metric design is to **minimize the number of different lab You can add static labels that always have fixed values. This is possible on two levels: * on the metrics registry (e.g. `Metrics.DefaultRegistry`) -* on one specific metric +* on a metric factory (e.g. `Metrics.WithLabels()`) -Both modes can be combined and instance-specific metric labels are also applied, as usual. +All levels of labeling can be combined and instance-specific metric labels can also be applied on top, as usual. Example with static labels on two levels and one instance-specific label: @@ -323,22 +307,126 @@ Metrics.DefaultRegistry.SetStaticLabels(new Dictionary { "environment", "testing" } }); -var requestsHandled = Metrics.CreateCounter("myapp_requests_handled_total", "Count of requests handled, labelled by response code.", - new CounterConfiguration - { - // Labels applied to all instances of myapp_requests_handled_total. - StaticLabels = new Dictionary - { - { "is_pci_compliant_environment", AppSettings.IsPciCompliant.ToString() } - }, - LabelNames = new[] { "response_code" } - }); +var backgroundServicesMetricFactory = Metrics.WithLabels(new Dictionary +{ + // Labels applied to all metrics created via this factory. + { "category", "background-services" } +}); + +var requestsHandled = backgroundServicesMetricFactory + .CreateCounter("myapp_requests_handled_total", "Count of requests handled, labelled by response code.", labelNames: new[] { "response_code" }); // Labels applied to individual instances of the metric. requestsHandled.WithLabels("404").Inc(); requestsHandled.WithLabels("200").Inc(); ``` +# Exemplars + +Exemplars facilitate [distributed tracing](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts), by attaching related trace IDs to metrics. This enables a metrics visualization app to cross-reference [traces](https://opentelemetry.io/docs/concepts/signals/traces/) that explain how the metric got the value it has. + +![](Exemplars.png) + +See also, [Grafana fundamentals - introduction to exemplars](https://grafana.com/docs/grafana/latest/fundamentals/exemplars/). + +By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels based on the current distributed tracing context (`Activity.Current`). If using OpenTelemetry tracing with ASP.NET Core, the `traceparent` HTTP request header will be used to automatically assign `Activity.Current`. + +```csharp +private static readonly Counter TotalSleepTime = Metrics + .CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); +... + +// You only need to create the Activity if one is not automatically assigned (e.g. by ASP.NET Core). +using (var activity = new Activity("Pausing before record processing").Start()) +{ + var sleepStopwatch = Stopwatch.StartNew(); + await Task.Delay(TimeSpan.FromSeconds(1)); + + // The trace_id and span_id from the current Activity are exposed as the exemplar. + TotalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); +} +``` + +This will be published as the following metric point: + +``` +sample_sleep_seconds_total 251.03833569999986 # {trace_id="08ad1c8cec52bf5284538abae7e6d26a",span_id="4761a4918922879b"} 1.0010688 1672634812.125 +``` + +You can override any default exemplar logic by providing your own exemplar when updating the value of the metric: + +```csharp +private static readonly Counter RecordsProcessed = Metrics + .CreateCounter("sample_records_processed_total", "Total number of records processed."); + +// The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. +private static readonly Exemplar.LabelKey RecordIdKey = Exemplar.Key("record_id"); +... + +foreach (var record in recordsToProcess) +{ + var exemplar = Exemplar.From(RecordIdKey.WithValue(record.Id.ToString())); + RecordsProcessed.Inc(exemplar); +} +``` + +> **Warning** +> Exemplars are limited to 128 ASCII characters (counting both keys and values) - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. + +Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. + +> **Note** +> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to [enable exemplar storage](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage), though. + +See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). + +# Limiting exemplar volume + +Exemplars can be expensive to store in the metrics database. For this reason, it can be useful to only record exemplars for "interesting" metric values. + +You can use `ExemplarBehavior.NewExemplarMinInterval` to define a minimum interval between exemplars - a new exemplar will only be recorded if this much time has passed. This can be useful to limit the rate of publishing unique exemplars. + +You can customize the default exemplar provider via `IMetricFactory.ExemplarBehavior` or `CounterConfiguration.ExemplarBehavior` and `HistogramConfiguration.ExemplarBehavior`, which allows you to provide your own method to generate exemplars and to filter which values/metrics exemplars are recorded for: + +Example of a custom exemplar provider used together with exemplar rate limiting: + +```csharp +// For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). +static Exemplar RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +{ + if (value < 0.1) + return Exemplar.None; + + return Exemplar.FromTraceContext(); +} + +var recordProcessingDuration = Metrics + .CreateHistogram("sample_record_processing_duration_seconds", "How long it took to process a record, in seconds.", + new HistogramConfiguration + { + Buckets = Histogram.PowersOfTenDividedBuckets(-4, 1, 5), + ExemplarBehavior = new() + { + DefaultExemplarProvider = RecordExemplarForSlowRecordProcessingDuration, + // Even if we have interesting data more often, do not record it to conserve exemplar storage. + NewExemplarMinInterval = TimeSpan.FromMinutes(5) + } + }); +``` + +For the ASP.NET Core HTTP server metrics, you can further fine-tune exemplar recording by inspecting the HTTP request and response: + +```csharp +app.UseHttpMetrics(options => +{ + options.ConfigureMeasurements(measurementOptions => + { + // Only measure exemplar if the HTTP response status code is not "OK". + measurementOptions.ExemplarPredicate = context => context.Response.StatusCode != HttpStatusCode.Ok; + }); +}); +``` + # When are metrics published? Metrics without labels are published immediately after the `Metrics.CreateX()` call. Metrics that use labels are published when you provide the label values for the first time. @@ -359,28 +447,87 @@ private static readonly Gauge UsersLoggedIn = Metrics UsersLoggedIn.Set(LoadSessions().Count); ``` -You can also use `.Publish()` on a metric to mark it as ready to be published without modifying the initial value (e.g. to publish a zero). +You can also use `.Publish()` on a metric to mark it as ready to be published without modifying the initial value (e.g. to publish a zero). Conversely, you can use `.Unpublish()` to hide a metric temporarily. Note that the metric remains in memory and retains its value. + +# Deleting metrics + +You can use `.Dispose()` or `.RemoveLabelled()` methods on the metric classes to manually delete metrics at any time. + +In some situations, it can be hard to determine when a metric with a specific set of labels becomes irrelevant and needs to be removed. The library provides some assistance here by enabling automatic expiration of metrics when they are no longer used. + +To enable automatic expiration, create the metrics via the metric factory returned by `Metrics.WithManagedLifetime()`. All such metrics will have a fixed expiration time, with the expiration restarting based on certain conditions that indicate the metric is in use. + +Option 1: metric lifetime can be controlled by leases - the metric expiration timer starts when the last lease is released (and will be reset when a new lease is taken again). + +```csharp +var factory = Metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromMinutes(5)); + +// With expiring metrics, we get back handles to the metric, not the metric directly. +var inProgressHandle = expiringMetricFactory + .CreateGauge("documents_in_progress", "Number of documents currently being processed.", + // Automatic metric deletion only makes sense if we have a high/unknown cardinality label set, + // so here is a sample label for each "document provider", whoever that may be. + labelNames: new[] { "document_provider" }); + +... + +public void ProcessDocument(string documentProvider) +{ + // Automatic metric deletion will not occur while this lease is held. + // This will also reset any existing expiration timer for this document provider. + inProgressHandle.WithLease(metric => + { + using (metric.TrackInProgress()) + DoDocumentProcessingWork(); + }, documentProvider); + // Lease is released here. + // If this was the last lease for this document provider, the expiration timer will now start. +} +``` + +Scenario 2: sometimes managing the leases is not required because you simply want the metric lifetime to be extended whenever the value is updated. + +```csharp +var factory = Metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromMinutes(5)); + +// With expiring metrics, we get back handles to the metric, not the metric directly. +var processingStartedHandle = expiringMetricFactory + .CreateGauge("documents_started_processing_total", "Number of documents for which processing has started.", + // Automatic metric deletion only makes sense if we have a high/unknown cardinality label set, + // so here is a sample label for each "document provider", whoever that may be. + labelNames: new[] { "document_provider" }); + +// This returns a metric instance that will reset the expiration timer whenever the metric value is updated. +var processingStarted = processingStartedHandle.WithExtendLifetimeOnUse(); + +... + +public void ProcessDocument(string documentProvider) +{ + // This will reset the expiration timer for this document provider. + processingStarted.WithLabels(documentProvider).Inc(); + + DoDocumentProcessingWork(); +} +``` + +The expiration logic is scoped to the factory. Multiple handles for the same metric from the same factory will share their expiration logic. However, handles for the same metric from different factories will have independent expiration logic. + +See also, [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs). # ASP.NET Core exporter middleware For projects built with ASP.NET Core, a middleware plugin is provided. -If you use the default Visual Studio project template, modify `Startup.cs` as follows: +If you use the default Visual Studio project templates, modify the `UseEndpoints` call as follows: -* ASP.NET Core 3 or newer - * Add `endpoints.MapMetrics()` to the endpoint configuration under `app.UseEndpoints`. -* ASP.NET Core 2 - * Add `app.UseMetricServer()` to the top of the `Configure` method. +* Add `endpoints.MapMetrics()` anywhere in the delegate body. ```csharp public void Configure(IApplicationBuilder app, ...) { - // ASP.NET Core 2 - app.UseMetricServer(); - // ... - // ASP.NET Core 3.1 or newer app.UseEndpoints(endpoints => { // ... @@ -394,6 +541,8 @@ The default configuration will publish metrics on the `/metrics` URL. The ASP.NET Core functionality is delivered in the `prometheus-net.AspNetCore` NuGet package. +See also, [Sample.Web](Sample.Web/Program.cs). + # ASP.NET Core HTTP request metrics The library exposes some metrics from ASP.NET Core applications: @@ -406,12 +555,9 @@ The ASP.NET Core functionality is delivered in the `prometheus-net.AspNetCore` N You can expose HTTP metrics by modifying your `Startup.Configure()` method: -* ASP.NET Core 3 or newer - * After `app.UseRouting()` add `app.UseHttpMetrics()`. -* ASP.NET Core 2 - * After `app.UseMetricServer()` add `app.UseHttpMetrics()`. +* After `app.UseRouting()` add `app.UseHttpMetrics()`. -Example `Startup.cs` (ASP.NET Core 3): +Example `Startup.cs`: ```csharp public void Configure(IApplicationBuilder app, ...) @@ -425,18 +571,42 @@ public void Configure(IApplicationBuilder app, ...) } ``` +By default, metrics are collected separately for each response status code (200, 201, 202, 203, ...). You can considerably reduce the size of the data set by only preserving information about the first digit of the status code: + +```csharp +app.UseHttpMetrics(options => +{ + // This will preserve only the first digit of the status code. + // For example: 200, 201, 203 -> 2xx + options.ReduceStatusCodeCardinality(); +}); +``` + NB! Exception handler middleware that changes HTTP response codes must be registered **after** `UseHttpMetrics()` in order to ensure that prometheus-net reports the correct HTTP response status code. -The `action` and `controller` route parameters are captured by default. You can include additional route parameters as follows: +The `action`, `controller` and `endpoint` route parameters are always captured by default. If Razor Pages is in use, the `page` label will be captured to show the path to the page. + +You can include additional route parameters as follows: ```csharp app.UseHttpMetrics(options => { - // This identifies the page when using Razor Pages. - options.AddRouteParameter("page"); + // Assume there exists a custom route parameter with this name. + options.AddRouteParameter("api-version"); }); ``` +You can also extract arbitrary data from the HttpContext into label values as follows: + +```csharp +app.UseHttpMetrics(options => +{ + options.AddCustomLabel("host", context => context.Request.Host.Host); +}); +``` + +See also, [Sample.Web](Sample.Web/Program.cs). + # ASP.NET Core gRPC request metrics The library allows you to expose some metrics from ASP.NET Core gRPC services. These metrics include labels for service and method name. @@ -460,6 +630,8 @@ public void Configure(IApplicationBuilder app, ...) The gRPC functionality is delivered in the `prometheus-net.AspNetCore.Grpc` NuGet package. +See also, [Sample.Grpc](Sample.Grpc/Program.cs). + # IHttpClientFactory metrics This library allows you to expose metrics about HttpClient instances created using [IHttpClientFactory](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests). @@ -471,20 +643,24 @@ The exposed metrics include: * Duration of HTTP client requests (from start of request to end of reading response headers). * Duration of HTTP client responses (from start of request to end of reading response body). -Example `Startup.cs` modification to enable these metrics: +Example `Startup.cs` modification to enable these metrics for all HttpClients registered in the service collection: ```csharp public void ConfigureServices(IServiceCollection services) { // ... - services.AddHttpClient(Options.DefaultName) - .UseHttpClientMetrics(); + services.UseHttpClientMetrics(); // ... } ``` +> **Note** +> You can also register HTTP client metrics only for a specific HttpClient by calling `services.AddHttpClient(...).UseHttpClientMetrics()`. + +See also, [Sample.Web](Sample.Web/Program.cs). + # ASP.NET Core health check status metrics You can expose the current status of [ASP.NET Core health checks](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks) as Prometheus metrics by extending your `IHealthChecksBuilder` in the `Startup.ConfigureServices()` method: @@ -504,24 +680,28 @@ public void ConfigureServices(IServiceCollection services, ...) } ``` +The status of each health check will be published in the `aspnetcore_healthcheck_status` metric. + The ASP.NET Core health check integration is delivered in the `prometheus-net.AspNetCore.HealthChecks` NuGet package. -# ASP.NET Core with basic authentication +See also, [Sample.Web](Sample.Web/Program.cs). -You may wish to restrict access to the metrics export URL. This can be accomplished using any ASP.NET Core authentication mechanism, as prometheus-net integrates directly into the composable ASP.NET Core request processing pipeline. +# Protecting the metrics endpoint from unauthorized access -For a simple example we can take [BasicAuthMiddleware by Johan Boström](https://www.johanbostrom.se/blog/adding-basic-auth-to-your-mvc-application-in-dotnet-core) which can be integrated by replacing the `app.UseMetricServer()` line with the following code block: +You may wish to restrict access to the metrics export URL. Documentation on how to apply ASP.NET Core security mechanisms is beyond the scope of this readme file but a good starting point may be to [require an authorization policy to be satisfied for accessing the endpoint](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0#apply-policies-to-endpoints) ```csharp -app.Map("/metrics", metricsApp => +app.UseEndpoints(endpoints => { - metricsApp.UseMiddleware("Contoso Corporation"); + // ... - // We already specified URL prefix in .Map() above, no need to specify it again here. - metricsApp.UseMetricServer(""); + // Assumes that you have previously configured the "ReadMetrics" policy (not shown). + endpoints.MapMetrics().RequireAuthorization("ReadMetrics"); }); ``` +Another commonly used option is to expose a separate web server endpoint (e.g. a new `KestrelMetricServer` instance) on a different port, with firewall rules limiting access to only certain IP addresses. Refer to the sample project [Sample.Web.DifferentPort](Sample.Web.DifferentPort/Program.cs). + # ASP.NET Web API exporter The easiest way to export metrics from an ASP.NET Web API app on the full .NET Framework is to use `AspNetMetricServer` in your `Global.asax.cs` file. Insert the following line to the top of the `Application_Start` method: @@ -550,6 +730,15 @@ metricServer.Start(); The default configuration will publish metrics on the `/metrics` URL. +If your app is an ASP.NET Core web app, you can use a pipeline-integrated mechanism: + +```csharp +services.AddMetricServer(options => +{ + options.Port = 1234; +}); +``` + # Publishing to Pushgateway Metrics can be posted to a [Pushgateway](https://prometheus.io/docs/practices/pushing/) server. @@ -564,6 +753,8 @@ var pusher = new MetricPusher(new MetricPusherOptions pusher.Start(); ``` +Note that the default behavior of the metric pusher is to append metrics. You can use `MetricPusherOptions.ReplaceOnPush` to make it replace existing metrics in the same group, removing any that are no longer pushed. + # Publishing to Pushgateway with basic authentication You can use a custom HttpClient to supply credentials for the Pushgateway. @@ -629,7 +820,7 @@ Metrics.DefaultRegistry.AddBeforeCollectCallback(async (cancel) => # Suppressing default metrics -The library provides some sample metrics about the current process out of the box, simply to ensure that some output is produced in a default configuration. If these metrics are not desirable you may remove them by calling `Metrics.SuppressDefaultMetrics()` before registering any of your own metrics. +The library enables various default metrics and integrations by default. If these default metrics are not desirable you may remove them by calling `Metrics.SuppressDefaultMetrics()` before registering any of your own metrics. # DiagnosticSource integration @@ -637,12 +828,12 @@ The library provides some sample metrics about the current process out of the bo ```csharp // An optional "options" parameter is available to customize adapter behavior. -var diagnosticSourceRegistration = DiagnosticSourceAdapter.StartListening(); +var registration = DiagnosticSourceAdapter.StartListening(); ... // Stops listening for DiagnosticSource events. -diagnosticSourceRegistration.Dispose(); +registration.Dispose(); ``` Any events that occur are exported as Prometheus metrics, indicating the name of the event source and the name of the event: @@ -653,3 +844,63 @@ diagnostic_events_total{source="HttpHandlerDiagnosticListener",event="System.Net ``` The level of detail obtained from this is rather low - only the total count for each event is exported. For more fine-grained analytics, you need to listen to DiagnosticSource events on your own and create custom metrics that can understand the meaning of each particular type of event that is of interest to you. + +# EventCounter integration + +> **Note** +> The output produced by this integration has changed significantly between prometheus-net 6.0 and prometheus-net 7.0. The old output format is no longer supported. + +[.NET Core provides the EventCounter mechanism for reporting diagnostic events](https://docs.microsoft.com/en-us/dotnet/core/diagnostics/event-counters), used used widely by .NET and ASP.NET Core classes. This library publishes all .NET EventCounter data by default. To suppress this, see [Suppressing default metrics](#suppressing-default-metrics). + +You can configure the integration using `Metrics.ConfigureEventCounterAdapter()`. + +By default, prometheus-net will only publish [the well-known .NET EventCounters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/available-counters) to minimize resource consumption in the default configuration. A custom event source filter must be provided in the configuration to enable publishing of additional event counters. + +See also, [Sample.Console](Sample.Console/Program.cs). + +# .NET Meters integration + +> **Note** +> The output produced by this integration has changed significantly between prometheus-net 6.0 and prometheus-net 7.0. The old output format is no longer supported. + +[.NET provides the Meters mechanism for reporting diagnostic metrics](https://docs.microsoft.com/en-us/dotnet/core/diagnostics/metrics). This library publishes all .NET Meters API data by default. To suppress this, see [Suppressing default metrics](#suppressing-default-metrics). + +You can configure the integration using `Metrics.ConfigureMeterAdapter()`. + +See also, [Sample.Console.DotNetMeters](Sample.Console.DotNetMeters/Program.cs). + +# Benchmarks + +A suite of benchmarks is included if you wish to explore the performance characteristics of the library. Simply build and run the `Benchmarks.NetCore` project in Release mode. + +As an example of the performance of measuring data using prometheus-net, we have the results of the MeasurementBenchmarks here, converted into measurements per second: + +| Metric type | Measurements per second | +|-------------------------|------------------------:| +| Counter | 261 million | +| Gauge | 591 million | +| Histogram (16 buckets) | 105 million | +| Histogram (128 buckets) | 65 million | + +Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. A representative result is here: + +| SDK | Benchmark scenario | CPU time | Memory | +|----------------|---------------------------------------|---------:|-------:| +| prometheus-net | Counter (existing timeseries) x100K | 230 µs | None | +| OpenTelemetry | Counter (existing timeseries) x100K | 10998 µs | None | +| prometheus-net | Histogram (existing timeseries) x100K | 957 µs | None | +| OpenTelemetry | Histogram (existing timeseries) x100K | 12110 µs | None | +| prometheus-net | Histogram (new timeseries) x1K | 716 µs | 664 KB | +| OpenTelemetry | Histogram (new timeseries) x1K | 350 µs | 96 KB | + +# Community projects + +Some useful related projects are: + +* [prometheus-net.DotNetRuntime](https://github.com/djluck/prometheus-net.DotNetRuntime) instruments .NET Core apps to export metrics on .NET Core performance. +* [prometheus-net.AspNet](https://github.com/rocklan/prometheus-net.AspNet) instruments ASP.NET full framework apps to export metrics on performance. +* [prometheus-net.SystemMetrics](https://github.com/Daniel15/prometheus-net.SystemMetrics) exports various system metrics such as CPU usage, disk usage, etc. +* [prometheus-net Grafana dashboards](https://github.com/prometheus-net/grafana-dashboards) provides example dashboards for visualizing prometheus-net metrics in [Grafana](https://grafana.com/). +* [PromQL.Parser](https://github.com/djluck/PromQL.Parser) enables you to parse and create Prometheus queries in C#. + +Note: to avoid confusion between "official" prometheus-net and community maintained packages, the `prometheus-net` namespace is protected on nuget.org. However, the `prometheus-net.Contrib.*` namespace allows package publishing by all authors. diff --git a/Resources/Nuspec/Merge-ReleaseNotes.ps1 b/Resources/Nuspec/Merge-ReleaseNotes.ps1 deleted file mode 100644 index 53de7c84..00000000 --- a/Resources/Nuspec/Merge-ReleaseNotes.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -$ErrorActionPreference = "Stop" - -# This script merges the History file into the NuSpec (using release agent paths). -# It just exists to enable maintaining the history as a plain text file, not hidden away somewhere. - -# Path as it exists on the release agent. -$historyPath = Join-Path $PSScriptRoot "..\History\History" -$releaseNotes = [IO.File]::ReadAllText($historyPath) - -foreach ($nuspec in Get-ChildItem -Path $PSScriptRoot -Filter *.nuspec) { - $content = [IO.File]::ReadAllText($nuspec.FullName) - $content = $content.Replace("", "$releaseNotes") - [IO.File]::WriteAllText($nuspec.FullName, $content) -} \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec b/Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec deleted file mode 100644 index 99189afa..00000000 --- a/Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec +++ /dev/null @@ -1,30 +0,0 @@ - - - - prometheus-net.AspNetCore.Grpc - sandersaares - ASP.NET Core gRPC integration with Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnetcore grpc - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec b/Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec deleted file mode 100644 index 8f1fb126..00000000 --- a/Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec +++ /dev/null @@ -1,34 +0,0 @@ - - - - prometheus-net.AspNetCore.HealthChecks - sandersaares - ASP.NET Core Health Checks integration with Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnetcore healthchecks - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.AspNetCore.nuspec b/Resources/Nuspec/prometheus-net.AspNetCore.nuspec deleted file mode 100644 index 665845e3..00000000 --- a/Resources/Nuspec/prometheus-net.AspNetCore.nuspec +++ /dev/null @@ -1,46 +0,0 @@ - - - - prometheus-net.AspNetCore - andrasm,qed-,lakario,sandersaares - ASP.NET Core middleware and stand-alone Kestrel server for exporting metrics to Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnetcore - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec b/Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec deleted file mode 100644 index db96a2e0..00000000 --- a/Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec +++ /dev/null @@ -1,32 +0,0 @@ - - - - prometheus-net.NetFramework.AspNet - sandersaares - ASP.NET Web API exporter for Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnet webapi - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.nuspec b/Resources/Nuspec/prometheus-net.nuspec deleted file mode 100644 index 15edc597..00000000 --- a/Resources/Nuspec/prometheus-net.nuspec +++ /dev/null @@ -1,41 +0,0 @@ - - - - prometheus-net - andrasm,qed-,lakario,sandersaares - .NET client library for the Prometheus monitoring and alerting system - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Resources.csproj b/Resources/Resources.csproj index fd8f26a0..0a044087 100644 --- a/Resources/Resources.csproj +++ b/Resources/Resources.csproj @@ -1,60 +1,6 @@ - - - - - Debug - AnyCPU - {608F1849-C417-4077-B60F-1BBF26762670} - Library - Properties - Resources - Resources - v4.6.1 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + net6.0 + Library + + diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index 7f5d03d9..1f300ba8 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,10 +2,10 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("5.0.2")] +[assembly: AssemblyFileVersion("8.2.1")] // Only use major version here, with others kept at zero, for correct assembly binding logic. -[assembly: AssemblyVersion("5.0.0")] +[assembly: AssemblyVersion("8.0.0")] [assembly: InternalsVisibleTo("Tests.NetFramework, PublicKey=002400000480000014010000060200000024000052534131000800000100010049b30b6bccc8311c8d5f9c006a5968b0592eca8b5a228e9e0a2ac0292e2a162ea3314b0f9941ffad9fe40a4071de2a0b6e4f50b70292d26081054f96df6a05e5a89a71538d50decaf8322f0cdd008e8e14d5e227b46c8c10a6cc850a5d7febf9ad5e0ffb8371e840744d3dd0cb88012ee61490a09d007fab29fc13fb0b4c2fb4d72692232546712b3e9e25a201e309bec907a9a241059d26f1826a337faf6e7a16902fc35e8dafeceff35a48622a9716af86138a1a064c879b7239a9495b8416abf63f8763a613e5be2e6b13403eb952c36008a281502bc2c89ca3367624b0791712f50674760fcbab2e7795fb6c53b0675f940d152ef449ad10463bce59a7d5")] [assembly: InternalsVisibleTo("Tests.NetCore, PublicKey=002400000480000014010000060200000024000052534131000800000100010049b30b6bccc8311c8d5f9c006a5968b0592eca8b5a228e9e0a2ac0292e2a162ea3314b0f9941ffad9fe40a4071de2a0b6e4f50b70292d26081054f96df6a05e5a89a71538d50decaf8322f0cdd008e8e14d5e227b46c8c10a6cc850a5d7febf9ad5e0ffb8371e840744d3dd0cb88012ee61490a09d007fab29fc13fb0b4c2fb4d72692232546712b3e9e25a201e309bec907a9a241059d26f1826a337faf6e7a16902fc35e8dafeceff35a48622a9716af86138a1a064c879b7239a9495b8416abf63f8763a613e5be2e6b13403eb952c36008a281502bc2c89ca3367624b0791712f50674760fcbab2e7795fb6c53b0675f940d152ef449ad10463bce59a7d5")] diff --git a/Resources/Nuspec/prometheus-net-logo.png b/Resources/prometheus-net-logo.png similarity index 100% rename from Resources/Nuspec/prometheus-net-logo.png rename to Resources/prometheus-net-logo.png diff --git a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs new file mode 100644 index 00000000..296c9d36 --- /dev/null +++ b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs @@ -0,0 +1,90 @@ +using System.Diagnostics.Metrics; + +/// +/// Sample custom metrics exported via the .NET Meters API. +/// +public static class CustomDotNetMeters +{ + public static void PublishSampleData() + { + // The meter object is the "container" for all the .NET metrics we will be publishing. + var meter1 = new Meter("Foobar.Wingwang.Dingdong", "vNext"); + + // Example metric: a simple counter. + var counter1 = meter1.CreateCounter("wings-wanged", "wings", "Counts the number of wings that have been wanged."); + + double nameCount = 1_000_000_000; + + double HowManyNamesAreThereInTheWorld() => nameCount++; + + var counter2 = meter1.CreateObservableCounter("all-names", HowManyNamesAreThereInTheWorld, "names", "Count of how many unique names exist in the world up to this point"); + + // Example metric: an observable gauge. + IEnumerable> ObserveGrossNestsAll() + { + foreach (var neepitKeepit in Enumerable.Range(1, 10)) + yield return new Measurement(Random.Shared.Next(800), new KeyValuePair("beek-beek", "yes"), new KeyValuePair("neepit-keepit", Random.Shared.Next(neepitKeepit))); + } + + var observableGauge1 = meter1.CreateObservableGauge("gross-nests", ObserveGrossNestsAll, "nests (gross)", "Measures the amount of nests nested (gross)."); + + // Example metric: a histogram. + var histogram1 = meter1.CreateHistogram("bytes-considered", "bytes", "Informs about all the bytes considered."); + + // .NET 7: Example metric: an up/down counter. + var upDown1 = meter1.CreateUpDownCounter("water-level", "brick-heights", "Current water level in the tank (measured in visible bricks from the midpoint)."); + + // Example metric: an observable up/down counter. + int sandLevel = 0; + + int MeasureSandLevel() + { + sandLevel += Random.Shared.Next(-1, 2); + return sandLevel; + } + + var upDown2 = meter1.CreateObservableUpDownCounter("sand-level", MeasureSandLevel, "chainlinks", "Current sand level in the tank (measured in visible chain links from the midpoint)."); + + // Example high cardinality metric: bytes sent per connection. + var highCardinalityCounter1 = meter1.CreateCounter("bytes-sent", "bytes", "Bytes sent per connection."); + + var activeConnections = new List(); + + // Start with 10 active connections. + foreach (var _ in Enumerable.Range(0, 10)) + activeConnections.Add(Guid.NewGuid()); + + // Dummy data generator. + _ = Task.Run(async delegate + { + while (true) + { + if (Random.Shared.Next(10) == 0) + counter1.Add(1, new KeyValuePair("wing-type", "FlexxWing MaxxFling 3000")); + + if (Random.Shared.Next(10) == 0) + counter1.Add(1, new KeyValuePair("wing-type", "SlaxxWing 1.0"), new KeyValuePair("wing-version", "beta")); + + // is-faulted here conflicts with the static label of the same name and gets overwritten by the static label. + histogram1.Record((byte)(Random.Shared.Next(256)), new KeyValuePair("is-faulted", true), new KeyValuePair("canbus_ver", "1.0")); + + // .NET 7 + upDown1.Add(Random.Shared.Next(-1, 2)); + + // Add some bytes for every active connection. + foreach (var connection in activeConnections) + highCardinalityCounter1.Add(Random.Shared.Next(10_000_000), new KeyValuePair("connection-id", connection)); + + // Maybe some connection went away, maybe some was added. + // Timeseries that stop receiving updates will disappear from prometheus-net output after a short delay (up to 10 minutes by default). + if (Random.Shared.Next(100) == 0) + { + activeConnections.RemoveAt(Random.Shared.Next(activeConnections.Count)); + activeConnections.Add(Guid.NewGuid()); + } + + await Task.Delay(100); + } + }); + } +} diff --git a/Sample.Console.DotNetMeters/Program.cs b/Sample.Console.DotNetMeters/Program.cs new file mode 100644 index 00000000..1137ca6c --- /dev/null +++ b/Sample.Console.DotNetMeters/Program.cs @@ -0,0 +1,34 @@ +using Prometheus; + +// This sample demonstrates how to publish data from the .NET Meters API as Prometheus metrics. +// +// NuGet packages required: +// * prometheus-net.AspNetCore + +// Suppress other default metrics to expose a cleaner sample data set with only the .NET Meters API data. +Metrics.SuppressDefaultMetrics(new SuppressDefaultMetricOptions +{ + SuppressProcessMetrics = true, + SuppressEventCounters = true, + SuppressDebugMetrics = true +}); + +// Example of static labels that conflict with .NET Meters API labels ("Bytes considered" histogram). +// Static labels overwrite values exported from the .NET Meters API, to resolve conflicting data. +Metrics.DefaultRegistry.SetStaticLabels(new Dictionary +{ + { "is_faulted", "false" } +}); + +// Start the metrics server on your preferred port number. +using var server = new KestrelMetricServer(port: 1234); +server.Start(); + +// Start publishing sample data via .NET Meters API. All data from the .NET Meters API is published by default. +CustomDotNetMeters.PublishSampleData(); + +// Metrics published in this sample: +// * custom metrics fed into the .NET Meters API from the CustomDotNetMeters class (enabled by default) +Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); +Console.WriteLine("Press enter to exit."); +Console.ReadLine(); \ No newline at end of file diff --git a/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj b/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj new file mode 100644 index 00000000..0ae9263f --- /dev/null +++ b/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj @@ -0,0 +1,21 @@ + + + + Exe + net7.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs new file mode 100644 index 00000000..3a9e6f6b --- /dev/null +++ b/Sample.Console.Exemplars/Program.cs @@ -0,0 +1,90 @@ +using Prometheus; +using System.Diagnostics; + +// This sample demonstrates how to attach exemplars to metrics exposed by a .NET console app. +// +// NuGet packages required: +// * prometheus-net.AspNetCore + +// Suppress some default metrics to make the output cleaner, so the exemplars are easier to see. +Metrics.SuppressDefaultMetrics(new SuppressDefaultMetricOptions +{ + SuppressEventCounters = true, + SuppressMeters = true, + SuppressProcessMetrics = true +}); + +// Start the metrics server on your preferred port number. +using var server = new KestrelMetricServer(port: 1234); +server.Start(); + +// Generate some sample data from fake business logic. +var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); +var recordSizeInPages = Metrics.CreateHistogram("sample_record_size_pages", "Size of a record, in pages.", new HistogramConfiguration +{ + Buckets = Histogram.PowersOfTenDividedBuckets(0, 2, 10) +}); + +// SAMPLED EXEMPLAR: For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). +static Exemplar RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +{ + if (value < 0.1) + return Exemplar.None; + + return Exemplar.FromTraceContext(); +} + +var recordProcessingDuration = Metrics.CreateHistogram("sample_record_processing_duration_seconds", "How long it took to process a record, in seconds.", new HistogramConfiguration +{ + Buckets = Histogram.PowersOfTenDividedBuckets(-4, 1, 5), + ExemplarBehavior = new() + { + DefaultExemplarProvider = RecordExemplarForSlowRecordProcessingDuration + } +}); + +var totalSleepTime = Metrics.CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); + +// CUSTOM EXEMPLAR: The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. +var recordIdKey = Exemplar.Key("record_id"); + +_ = Task.Run(async delegate +{ + while (true) + { + // DEFAULT EXEMPLAR: We expose the trace_id and span_id for distributed tracing, based on Activity.Current. + // Activity.Current is often automatically inherited from incoming HTTP requests if using OpenTelemetry tracing with ASP.NET Core. + // Here, we manually create and start an activity for sample purposes, without relying on the platform managing the activity context. + // See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts + using (var activity = new Activity("Pausing before record processing").Start()) + { + var sleepStopwatch = Stopwatch.StartNew(); + await Task.Delay(TimeSpan.FromSeconds(1)); + + // The trace_id and span_id from the current Activity are exposed as the exemplar by default. + totalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); + } + + using var processingDurationTimer = recordProcessingDuration.NewTimer(); + + // Pretend to process a record approximately every second, just for changing sample data. + var recordId = Guid.NewGuid(); + var recordPageCount = Random.Shared.Next(minValue: 5, maxValue: 100); + + // CUSTOM EXEMPLAR: We pass the record ID key-value pair when we increment the metric. + // When the metric data is published to Prometheus, the most recent record ID will be attached to it. + var exemplar = Exemplar.From(recordIdKey.WithValue(recordId.ToString())); + + // Note that one Exemplar object can only be used once. You must clone it to reuse it. + recordsProcessed.Inc(exemplar.Clone()); + recordSizeInPages.Observe(recordPageCount, exemplar); + } +}); + +// Metrics published in this sample: +// * the custom sample metrics defined above, with exemplars +// * internal debug metrics from prometheus-net, without exemplars +// Note that the OpenMetrics exposition format must be selected via HTTP header or query string parameter to see exemplars. +Console.WriteLine("Open http://localhost:1234/metrics?accept=application/openmetrics-text in a web browser."); +Console.WriteLine("Press enter to exit."); +Console.ReadLine(); \ No newline at end of file diff --git a/Sample.Console.Exemplars/Sample.Console.Exemplars.csproj b/Sample.Console.Exemplars/Sample.Console.Exemplars.csproj new file mode 100644 index 00000000..62836eb9 --- /dev/null +++ b/Sample.Console.Exemplars/Sample.Console.Exemplars.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Console.NetFramework/App.config b/Sample.Console.NetFramework/App.config new file mode 100644 index 00000000..b50c74f3 --- /dev/null +++ b/Sample.Console.NetFramework/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Sample.Console.NetFramework/Program.cs b/Sample.Console.NetFramework/Program.cs new file mode 100644 index 00000000..a3cdc50e --- /dev/null +++ b/Sample.Console.NetFramework/Program.cs @@ -0,0 +1,54 @@ +using Prometheus; +using System; +using System.Net; +using System.Threading.Tasks; + +// This sample demonstrates how to integrate prometheus-net into a console app (e.g. a worker service). +// +// NuGet packages required: +// * prometheus-net + +internal static class Program +{ + public static void Main(string[] args) + { + // Start the metrics server on your preferred port number. + var server = new MetricServer(port: 1234); + + try + { + // On .NET Framework, starting the server requires either elevation to Administrator or permission configuration. + server.Start(); + } + catch (HttpListenerException ex) + { + Console.WriteLine($"Failed to start metric server: {ex.Message}"); + Console.WriteLine("You may need to grant permissions to your user account if not running as Administrator:"); + Console.WriteLine("netsh http add urlacl url=http://+:1234/metrics user=DOMAIN\\user"); + return; + } + + // Generate some sample data from fake business logic. + var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); + + _ = Task.Run(async delegate + { + while (true) + { + // Pretend to process a record approximately every second, just for changing sample data. + recordsProcessed.Inc(); + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + }); + + // Metrics published in this sample: + // * built-in process metrics giving basic information about the .NET runtime (enabled by default) + // * the sample counter defined above + Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); + Console.WriteLine("Press enter to exit."); + Console.ReadLine(); + + server.Stop(); + } +} \ No newline at end of file diff --git a/Sample.Console.NetFramework/Properties/AssemblyInfo.cs b/Sample.Console.NetFramework/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..229ed5c6 --- /dev/null +++ b/Sample.Console.NetFramework/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Sample.Console.NetFramework")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Sample.Console.NetFramework")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e4d772af-bfa0-4ea9-ae3c-c7326a42f1e5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Sample.Console.NetFramework/Sample.Console.NetFramework.csproj b/Sample.Console.NetFramework/Sample.Console.NetFramework.csproj new file mode 100644 index 00000000..af0bbe24 --- /dev/null +++ b/Sample.Console.NetFramework/Sample.Console.NetFramework.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {E4D772AF-BFA0-4EA9-AE3C-C7326A42F1E5} + Exe + Sample.Console.NetFramework + Sample.Console.NetFramework + v4.6.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + false + true + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + false + true + + + + + + + + + + + + + + + + + + + + + {e585417c-f7dd-4d8c-a0c5-d4b79594634a} + Prometheus + + + + \ No newline at end of file diff --git a/Sample.Console.NoAspNetCore/Program.cs b/Sample.Console.NoAspNetCore/Program.cs new file mode 100644 index 00000000..cd2ae775 --- /dev/null +++ b/Sample.Console.NoAspNetCore/Program.cs @@ -0,0 +1,46 @@ +using Prometheus; +using System.Net; + +// This sample demonstrates how to integrate prometheus-net into a console app (e.g. a worker service) in a manner +// that does not require the ASP.NET Core runtime to be installed, as may be the case in some special hosting environments. +// +// NuGet packages required: +// * prometheus-net + +// Start the metrics server on your preferred port number. +using var server = new MetricServer(port: 1234); + +try +{ + server.Start(); +} +catch (HttpListenerException ex) +{ + Console.WriteLine($"Failed to start metric server: {ex.Message}"); + Console.WriteLine("You may need to grant permissions to your user account if not running as Administrator:"); + Console.WriteLine("netsh http add urlacl url=http://+:1234/metrics user=DOMAIN\\user"); + return; +} + +// Generate some sample data from fake business logic. +var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); + +_ = Task.Run(async delegate +{ + while (true) + { + // Pretend to process a record approximately every second, just for changing sample data. + recordsProcessed.Inc(); + + await Task.Delay(TimeSpan.FromSeconds(1)); + } +}); + +// Metrics published in this sample: +// * built-in process metrics giving basic information about the .NET runtime (enabled by default) +// * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) +// * metrics from .NET Meters (enabled by default) +// * the custom sample counter defined above +Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); +Console.WriteLine("Press enter to exit."); +Console.ReadLine(); \ No newline at end of file diff --git a/Sample.Console.NoAspNetCore/Sample.Console.NoAspNetCore.csproj b/Sample.Console.NoAspNetCore/Sample.Console.NoAspNetCore.csproj new file mode 100644 index 00000000..396ea3e9 --- /dev/null +++ b/Sample.Console.NoAspNetCore/Sample.Console.NoAspNetCore.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Console/Program.cs b/Sample.Console/Program.cs new file mode 100644 index 00000000..cf30871a --- /dev/null +++ b/Sample.Console/Program.cs @@ -0,0 +1,34 @@ +using Prometheus; + +// This sample demonstrates how to integrate prometheus-net into a console app (e.g. a worker service). +// +// NuGet packages required: +// * prometheus-net.AspNetCore + +// Start the metrics server on your preferred port number. +using var server = new KestrelMetricServer(port: 1234); +server.Start(); + +// Generate some sample data from fake business logic. +var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); + +_ = Task.Run(async delegate +{ + while (true) + { + // Pretend to process a record approximately every second, just for changing sample data. + recordsProcessed.Inc(); + + await Task.Delay(TimeSpan.FromSeconds(1)); + } +}); + +// Metrics published in this sample: +// * built-in process metrics giving basic information about the .NET runtime (enabled by default) +// * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) +// * metrics from .NET Meters (enabled by default) +// * prometheus-net self-inspection metrics that indicate number of registered metrics/timeseries (enabled by default) +// * the custom sample counter defined above +Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); +Console.WriteLine("Press enter to exit."); +Console.ReadLine(); \ No newline at end of file diff --git a/Sample.Console/Sample.Console.csproj b/Sample.Console/Sample.Console.csproj new file mode 100644 index 00000000..62836eb9 --- /dev/null +++ b/Sample.Console/Sample.Console.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Grpc.Client/Program.cs b/Sample.Grpc.Client/Program.cs new file mode 100644 index 00000000..d38a1b94 --- /dev/null +++ b/Sample.Grpc.Client/Program.cs @@ -0,0 +1,11 @@ +using Grpc.Net.Client; +using Sample.Grpc; + +// The port number must match the HTTPS port in Sample.Grpc/Properties/launchSettings.json +using var channel = GrpcChannel.ForAddress("https://localhost:7124"); + +var client = new Greeter.GreeterClient(channel); +var reply = await client.SayHelloAsync(new HelloRequest { Name = "Sample.Grpc.Client" }); +Console.WriteLine("Reply received from gRPC service: " + reply.Message); +Console.WriteLine("Press any key to exit..."); +Console.ReadKey(); \ No newline at end of file diff --git a/Sample.Grpc.Client/Sample.Grpc.Client.csproj b/Sample.Grpc.Client/Sample.Grpc.Client.csproj new file mode 100644 index 00000000..1d988346 --- /dev/null +++ b/Sample.Grpc.Client/Sample.Grpc.Client.csproj @@ -0,0 +1,37 @@ + + + + Exe + net6.0 + + enable + enable + True + True + + 1591;8981 + + latest + 9999 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Protos\greet.proto + + + + diff --git a/Sample.Grpc/Program.cs b/Sample.Grpc/Program.cs new file mode 100644 index 00000000..d78de82f --- /dev/null +++ b/Sample.Grpc/Program.cs @@ -0,0 +1,42 @@ +using Prometheus; +using Sample.Grpc.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddGrpc(); + +var app = builder.Build(); + +// Enable routing, which is necessary to both: +// 1) capture metadata about gRPC requests, to add to the labels. +// 2) expose /metrics in the same pipeline. +app.UseRouting(); + +// Capture metrics about received gRPC requests. +app.UseGrpcMetrics(); + +// Capture metrics about received HTTP requests. +app.UseHttpMetrics(); + +// The sample gRPC service. Use Sample.Grpc.Client to call this service and capture sample metrics. +app.MapGrpcService(); + +app.UseEndpoints(endpoints => +{ + // Enable the /metrics page to export Prometheus metrics. + // Open http://localhost:5025/metrics to see the metrics. + // + // Metrics published in this sample: + // * built-in process metrics giving basic information about the .NET runtime (enabled by default) + // * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) + // * metrics from .NET Meters (enabled by default) + // * prometheus-net self-inspection metrics that indicate number of registered metrics/timeseries (enabled by default) + // * metrics about HTTP requests handled by the web app (configured above) + // * metrics about gRPC requests handled by the web app (configured above) + app.MapMetrics(); +}); + +// Access the root URL to generate sample data about non-gRPC HTTP requests handled by the app. +app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. Use the Sample.Grpc.Client app to communicate with the service."); + +app.Run(); diff --git a/Sample.Grpc/Properties/launchSettings.json b/Sample.Grpc/Properties/launchSettings.json new file mode 100644 index 00000000..d8429ba4 --- /dev/null +++ b/Sample.Grpc/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Sample.Grpc": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5025;https://localhost:7124", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Sample.Grpc/Protos/greet.proto b/Sample.Grpc/Protos/greet.proto new file mode 100644 index 00000000..75ff293c --- /dev/null +++ b/Sample.Grpc/Protos/greet.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +option csharp_namespace = "Sample.Grpc"; + +package greet; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings. +message HelloReply { + string message = 1; +} diff --git a/Sample.Grpc/Sample.Grpc.csproj b/Sample.Grpc/Sample.Grpc.csproj new file mode 100644 index 00000000..ff59789f --- /dev/null +++ b/Sample.Grpc/Sample.Grpc.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + + enable + enable + True + True + + 1591;8981 + + latest + 9999 + + + + + + + + + + + + + + + + diff --git a/Sample.Grpc/Services/GreeterService.cs b/Sample.Grpc/Services/GreeterService.cs new file mode 100644 index 00000000..d310a250 --- /dev/null +++ b/Sample.Grpc/Services/GreeterService.cs @@ -0,0 +1,21 @@ +using Grpc.Core; + +namespace Sample.Grpc.Services +{ + public class GreeterService : Greeter.GreeterBase + { + private readonly ILogger _logger; + public GreeterService(ILogger logger) + { + _logger = logger; + } + + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply + { + Message = "Hello " + request.Name + }); + } + } +} \ No newline at end of file diff --git a/Sample.Grpc/appsettings.Development.json b/Sample.Grpc/appsettings.Development.json new file mode 100644 index 00000000..dff7506a --- /dev/null +++ b/Sample.Grpc/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:5025" + }, + "Https": { + "Url": "https://localhost:7124" + } + } + } +} diff --git a/Sample.Grpc/appsettings.json b/Sample.Grpc/appsettings.json new file mode 100644 index 00000000..5f8ccb66 --- /dev/null +++ b/Sample.Grpc/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http1AndHttp2" + } + } +} diff --git a/Sample.NetStandard/ImportantProcess.cs b/Sample.NetStandard/ImportantProcess.cs new file mode 100644 index 00000000..9f96d77c --- /dev/null +++ b/Sample.NetStandard/ImportantProcess.cs @@ -0,0 +1,21 @@ +using Prometheus; + +namespace Sample.NetStandard; + +public static class ImportantProcess +{ + public static void Start() + { + _ = Task.Run(async delegate + { + while (true) + { + ImportantCounter.Inc(); + + await Task.Delay(TimeSpan.FromSeconds(0.1)); + } + }); + } + + private static readonly Counter ImportantCounter = Metrics.CreateCounter("sample_important_counter", "Counts up and up and up!"); +} diff --git a/Sample.NetStandard/Sample.NetStandard.csproj b/Sample.NetStandard/Sample.NetStandard.csproj new file mode 100644 index 00000000..fb1cf430 --- /dev/null +++ b/Sample.NetStandard/Sample.NetStandard.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Web.DifferentPort/Pages/Error.cshtml b/Sample.Web.DifferentPort/Pages/Error.cshtml new file mode 100644 index 00000000..6f92b956 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/Sample.Web.DifferentPort/Pages/Error.cshtml.cs b/Sample.Web.DifferentPort/Pages/Error.cshtml.cs new file mode 100644 index 00000000..99ecfda3 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Error.cshtml.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Diagnostics; + +namespace Sample.Web.DifferentPort.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [IgnoreAntiforgeryToken] + public class ErrorModel : PageModel + { + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} \ No newline at end of file diff --git a/Sample.Web.DifferentPort/Pages/Index.cshtml b/Sample.Web.DifferentPort/Pages/Index.cshtml new file mode 100644 index 00000000..b5f0c15f --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Index.cshtml @@ -0,0 +1,10 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+
diff --git a/Sample.Web.DifferentPort/Pages/Index.cshtml.cs b/Sample.Web.DifferentPort/Pages/Index.cshtml.cs new file mode 100644 index 00000000..368a145e --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Index.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Sample.Web.DifferentPort.Pages +{ + public class IndexModel : PageModel + { + private readonly ILogger _logger; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/Sample.Web.DifferentPort/Pages/Privacy.cshtml b/Sample.Web.DifferentPort/Pages/Privacy.cshtml new file mode 100644 index 00000000..46ba9661 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/Sample.Web.DifferentPort/Pages/Privacy.cshtml.cs b/Sample.Web.DifferentPort/Pages/Privacy.cshtml.cs new file mode 100644 index 00000000..11b48348 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Privacy.cshtml.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Sample.Web.DifferentPort.Pages +{ + public class PrivacyModel : PageModel + { + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } + } +} \ No newline at end of file diff --git a/Sample.Web.DifferentPort/Pages/Shared/_Layout.cshtml b/Sample.Web.DifferentPort/Pages/Shared/_Layout.cshtml new file mode 100644 index 00000000..0a197e27 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Shared/_Layout.cshtml @@ -0,0 +1,51 @@ + + + + + + @ViewData["Title"] - Sample.Web.DifferentPort + + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © 2022 - Sample.Web.DifferentPort - Privacy +
+
+ + + + + + @await RenderSectionAsync("Scripts", required: false) + + \ No newline at end of file diff --git a/Sample.Web.DifferentPort/Pages/Shared/_Layout.cshtml.css b/Sample.Web.DifferentPort/Pages/Shared/_Layout.cshtml.css new file mode 100644 index 00000000..a72cbeaf --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Shared/_Layout.cshtml.css @@ -0,0 +1,48 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +a { + color: #0077cc; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; +} diff --git a/Sample.Web.DifferentPort/Pages/Shared/_ValidationScriptsPartial.cshtml b/Sample.Web.DifferentPort/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 00000000..5a16d80a --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/Sample.Web.DifferentPort/Pages/_ViewImports.cshtml b/Sample.Web.DifferentPort/Pages/_ViewImports.cshtml new file mode 100644 index 00000000..c31624a8 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using Sample.Web.DifferentPort +@namespace Sample.Web.DifferentPort.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Sample.Web.DifferentPort/Pages/_ViewStart.cshtml b/Sample.Web.DifferentPort/Pages/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/Sample.Web.DifferentPort/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/Sample.Web.DifferentPort/Program.cs b/Sample.Web.DifferentPort/Program.cs new file mode 100644 index 00000000..794fee35 --- /dev/null +++ b/Sample.Web.DifferentPort/Program.cs @@ -0,0 +1,44 @@ +// This sample demonstrates how to integrate prometheus-net into a web app while instructing it +// to export metrics on a dedicated port (e.g. so it can be firewalled off from the internet). +// +// NuGet packages required: +// * prometheus-net.AspNetCore + +using Prometheus; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorPages(); + +// Start the metrics exporter as a background service. +// Open http://localhost:5167 to see the web app. +// Open http://localhost:1234/metrics to see the metrics. +// +// Metrics published: +// * built-in process metrics giving basic information about the .NET runtime (enabled by default) +// * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) +// * metrics from .NET Meters (enabled by default) +// * metrics about requests handled by the web app (configured below) +builder.Services.AddMetricServer(options => +{ + options.Port = 1234; +}); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); +} +app.UseStaticFiles(); + +app.UseRouting(); + +// Capture metrics about all received HTTP requests. +app.UseHttpMetrics(); + +app.UseAuthorization(); + +app.MapRazorPages(); + +app.Run(); diff --git a/Sample.Web.DifferentPort/Properties/launchSettings.json b/Sample.Web.DifferentPort/Properties/launchSettings.json new file mode 100644 index 00000000..7ad2bb94 --- /dev/null +++ b/Sample.Web.DifferentPort/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Sample.Web.DifferentPort": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5167", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Sample.Web.DifferentPort/Sample.Web.DifferentPort.csproj b/Sample.Web.DifferentPort/Sample.Web.DifferentPort.csproj new file mode 100644 index 00000000..6c138b96 --- /dev/null +++ b/Sample.Web.DifferentPort/Sample.Web.DifferentPort.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Web.DifferentPort/appsettings.Development.json b/Sample.Web.DifferentPort/appsettings.Development.json new file mode 100644 index 00000000..bfc049b4 --- /dev/null +++ b/Sample.Web.DifferentPort/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:5167" + } + } + } +} diff --git a/Sample.Web.DifferentPort/appsettings.json b/Sample.Web.DifferentPort/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Sample.Web.DifferentPort/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Sample.Web.DifferentPort/wwwroot/css/site.css b/Sample.Web.DifferentPort/wwwroot/css/site.css new file mode 100644 index 00000000..f27e5ad2 --- /dev/null +++ b/Sample.Web.DifferentPort/wwwroot/css/site.css @@ -0,0 +1,18 @@ +html { + font-size: 14px; +} + +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +html { + position: relative; + min-height: 100%; +} + +body { + margin-bottom: 60px; +} \ No newline at end of file diff --git a/Sample.Web.DifferentPort/wwwroot/favicon.ico b/Sample.Web.DifferentPort/wwwroot/favicon.ico new file mode 100644 index 00000000..63e859b4 Binary files /dev/null and b/Sample.Web.DifferentPort/wwwroot/favicon.ico differ diff --git a/Sample.Web.DifferentPort/wwwroot/js/site.js b/Sample.Web.DifferentPort/wwwroot/js/site.js new file mode 100644 index 00000000..ac49c186 --- /dev/null +++ b/Sample.Web.DifferentPort/wwwroot/js/site.js @@ -0,0 +1,4 @@ +// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +// for details on configuring this project to bundle and minify static web assets. + +// Write your JavaScript code. diff --git a/Sample.Web.MetricExpiration/Pages/Error.cshtml b/Sample.Web.MetricExpiration/Pages/Error.cshtml new file mode 100644 index 00000000..6f92b956 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/Sample.Web.MetricExpiration/Pages/Error.cshtml.cs b/Sample.Web.MetricExpiration/Pages/Error.cshtml.cs new file mode 100644 index 00000000..5d083860 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Error.cshtml.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Diagnostics; + +namespace Sample.Web.MetricExpiration.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [IgnoreAntiforgeryToken] + public class ErrorModel : PageModel + { + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} \ No newline at end of file diff --git a/Sample.Web.MetricExpiration/Pages/Index.cshtml b/Sample.Web.MetricExpiration/Pages/Index.cshtml new file mode 100644 index 00000000..b5f0c15f --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Index.cshtml @@ -0,0 +1,10 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+
diff --git a/Sample.Web.MetricExpiration/Pages/Index.cshtml.cs b/Sample.Web.MetricExpiration/Pages/Index.cshtml.cs new file mode 100644 index 00000000..26a6615a --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Index.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Sample.Web.MetricExpiration.Pages +{ + public class IndexModel : PageModel + { + private readonly ILogger _logger; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/Sample.Web.MetricExpiration/Pages/Privacy.cshtml b/Sample.Web.MetricExpiration/Pages/Privacy.cshtml new file mode 100644 index 00000000..46ba9661 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/Sample.Web.MetricExpiration/Pages/Privacy.cshtml.cs b/Sample.Web.MetricExpiration/Pages/Privacy.cshtml.cs new file mode 100644 index 00000000..e54ad6e8 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Privacy.cshtml.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Sample.Web.MetricExpiration.Pages +{ + public class PrivacyModel : PageModel + { + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } + } +} \ No newline at end of file diff --git a/Sample.Web.MetricExpiration/Pages/Shared/_Layout.cshtml b/Sample.Web.MetricExpiration/Pages/Shared/_Layout.cshtml new file mode 100644 index 00000000..bb6b9768 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Shared/_Layout.cshtml @@ -0,0 +1,51 @@ + + + + + + @ViewData["Title"] - Sample.Web.MetricExpiration + + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © 2022 - Sample.Web.MetricExpiration - Privacy +
+
+ + + + + + @await RenderSectionAsync("Scripts", required: false) + + \ No newline at end of file diff --git a/Sample.Web.MetricExpiration/Pages/Shared/_Layout.cshtml.css b/Sample.Web.MetricExpiration/Pages/Shared/_Layout.cshtml.css new file mode 100644 index 00000000..a72cbeaf --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Shared/_Layout.cshtml.css @@ -0,0 +1,48 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +a { + color: #0077cc; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; +} diff --git a/Sample.Web.MetricExpiration/Pages/Shared/_ValidationScriptsPartial.cshtml b/Sample.Web.MetricExpiration/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 00000000..5a16d80a --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/Sample.Web.MetricExpiration/Pages/_ViewImports.cshtml b/Sample.Web.MetricExpiration/Pages/_ViewImports.cshtml new file mode 100644 index 00000000..296f1d25 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using Sample.Web.MetricExpiration +@namespace Sample.Web.MetricExpiration.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Sample.Web.MetricExpiration/Pages/_ViewStart.cshtml b/Sample.Web.MetricExpiration/Pages/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/Sample.Web.MetricExpiration/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/Sample.Web.MetricExpiration/Program.cs b/Sample.Web.MetricExpiration/Program.cs new file mode 100644 index 00000000..c8459e53 --- /dev/null +++ b/Sample.Web.MetricExpiration/Program.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Prometheus; +using Prometheus.HttpMetrics; + +// This sample demonstrates how to integrate prometheus-net into a web app. +// +// NuGet packages required: +// * prometheus-net.AspNetCore + +// Let's suppress the default metrics that are built-in, to more easily see the changing metrics output. +Metrics.SuppressDefaultMetrics(); + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); +} +app.UseStaticFiles(); + +app.UseRouting(); + +// Use an auto-expiring variant for all the demo metrics here - they get automatically deleted if not used in the last 60 seconds. +var expiringMetricFactory = Metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromSeconds(60)); + +// OPTION 1: metric lifetime can be managed by leases, to ensure they do not go away during potentially +// long-running operations but go away quickly when the operation is not running anymore (e.g. "in progress" type metrics). +_ = Task.Run(async delegate +{ + var inProgress = expiringMetricFactory.CreateGauge("long_running_operations_in_progress", "Number of long running operations in progress.", labelNames: new[] { "operation_type" }); + + // The metric will not be deleted as long as this lease is kept. + await inProgress.WithLeaseAsync(async inProgressInstance => + { + // Long-running operation, which we track via the "in progress" gauge. + using (inProgressInstance.TrackInProgress()) + await Task.Delay(TimeSpan.FromSeconds(30)); + }, "VeryLongOperation"); + + // Just to let you know when to look at it. + Console.WriteLine("Long-running operation has finished."); + + // Done! Now the metric lease will be released and soon, the metric will expire and be removed. +}); + +// OPTION 2: metrics can auto-extend lifetime whenever their values are updated. +app.UseHttpMetrics(options => +{ + // Here we do something that is typically a no-no in terms of best practices (and GDPR?): we record every unique URL! + // We use metric expiration to keep the set of metrics in-memory limited to only recently used URLs, which limits the likelihood + // of our web server getting DoSed. We will still need a very very beefy metrics database to actually handle so much data, + // so this is not a good idea even if we manage to bypass the most obvious stumbling block of running out of memory! + options.AddCustomLabel("url", context => context.Request.GetDisplayUrl()); + + options.InProgress.Gauge = expiringMetricFactory.CreateGauge( + "http_requests_in_progress", + "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.", + // Let's say that we have all the labels present, as automatic label set selection does not work if we use a custom metric. + labelNames: HttpRequestLabelNames.All + // ... except for "Code" which is only possible to identify after the request is already finished. + .Except(new[] { "code" }) + // ... plus the custom "url" label that we defined above. + .Concat(new[] { "url" }) + .ToArray()) + .WithExtendLifetimeOnUse(); + + options.RequestCount.Counter = expiringMetricFactory.CreateCounter( + "http_requests_received_total", + "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.", + // Let's say that we have all the labels present, as automatic label set selection does not work if we use a custom metric. + labelNames: HttpRequestLabelNames.All + // ... plus the custom "url" label that we defined above. + .Concat(new[] { "url" }) + .ToArray()) + .WithExtendLifetimeOnUse(); + + options.RequestDuration.Histogram = expiringMetricFactory.CreateHistogram( + "http_request_duration_seconds", + "The duration of HTTP requests processed by an ASP.NET Core application.", + // Let's say that we have all the labels present, as automatic label set selection does not work if we use a custom metric. + labelNames: HttpRequestLabelNames.All + // ... plus the custom "url" label that we defined above. + .Concat(new[] { "url" }) + .ToArray(), + new HistogramConfiguration + { + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16) + }) + .WithExtendLifetimeOnUse(); +}); + +app.UseAuthorization(); + +app.MapRazorPages(); + +app.UseEndpoints(endpoints => +{ + // Enable the /metrics page to export Prometheus metrics. + // Open http://localhost:5283/metrics to see the metrics. + // + // Metrics published in this sample: + // * metrics about requests handled by the web app (configured above) + // * custom metrics about long-running operations, defined above + // + // To try out the expiration feature, navigate to different pages of the app (e.g. between Home and Privacy) and + // observe how the metrics for accessed URLs disappear a minute after the URL was last used. + endpoints.MapMetrics(); +}); + +app.Run(); diff --git a/Sample.Web.MetricExpiration/Properties/launchSettings.json b/Sample.Web.MetricExpiration/Properties/launchSettings.json new file mode 100644 index 00000000..ec1395a4 --- /dev/null +++ b/Sample.Web.MetricExpiration/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Sample.Web.MetricExpiration": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Sample.Web.MetricExpiration/Sample.Web.MetricExpiration.csproj b/Sample.Web.MetricExpiration/Sample.Web.MetricExpiration.csproj new file mode 100644 index 00000000..c6c5fb06 --- /dev/null +++ b/Sample.Web.MetricExpiration/Sample.Web.MetricExpiration.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Web.MetricExpiration/appsettings.Development.json b/Sample.Web.MetricExpiration/appsettings.Development.json new file mode 100644 index 00000000..f1137bf3 --- /dev/null +++ b/Sample.Web.MetricExpiration/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:5283" + } + } + } +} diff --git a/Sample.Web.MetricExpiration/appsettings.json b/Sample.Web.MetricExpiration/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Sample.Web.MetricExpiration/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Sample.Web.MetricExpiration/wwwroot/css/site.css b/Sample.Web.MetricExpiration/wwwroot/css/site.css new file mode 100644 index 00000000..f27e5ad2 --- /dev/null +++ b/Sample.Web.MetricExpiration/wwwroot/css/site.css @@ -0,0 +1,18 @@ +html { + font-size: 14px; +} + +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +html { + position: relative; + min-height: 100%; +} + +body { + margin-bottom: 60px; +} \ No newline at end of file diff --git a/Sample.Web.MetricExpiration/wwwroot/favicon.ico b/Sample.Web.MetricExpiration/wwwroot/favicon.ico new file mode 100644 index 00000000..63e859b4 Binary files /dev/null and b/Sample.Web.MetricExpiration/wwwroot/favicon.ico differ diff --git a/Sample.Web.MetricExpiration/wwwroot/js/site.js b/Sample.Web.MetricExpiration/wwwroot/js/site.js new file mode 100644 index 00000000..ac49c186 --- /dev/null +++ b/Sample.Web.MetricExpiration/wwwroot/js/site.js @@ -0,0 +1,4 @@ +// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +// for details on configuring this project to bundle and minify static web assets. + +// Write your JavaScript code. diff --git a/Sample.Web.NetFramework/App_Start/BundleConfig.cs b/Sample.Web.NetFramework/App_Start/BundleConfig.cs new file mode 100644 index 00000000..1f2236c9 --- /dev/null +++ b/Sample.Web.NetFramework/App_Start/BundleConfig.cs @@ -0,0 +1,30 @@ +using System.Web; +using System.Web.Optimization; + +namespace Sample.Web.NetFramework +{ + public class BundleConfig + { + // For more information on bundling, visit https://go.microsoft.com/fwlink/?LinkId=301862 + public static void RegisterBundles(BundleCollection bundles) + { + bundles.Add(new ScriptBundle("~/bundles/jquery").Include( + "~/Scripts/jquery-{version}.js")); + + bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( + "~/Scripts/jquery.validate*")); + + // Use the development version of Modernizr to develop with and learn from. Then, when you're + // ready for production, use the build tool at https://modernizr.com to pick only the tests you need. + bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( + "~/Scripts/modernizr-*")); + + bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( + "~/Scripts/bootstrap.js")); + + bundles.Add(new StyleBundle("~/Content/css").Include( + "~/Content/bootstrap.css", + "~/Content/site.css")); + } + } +} diff --git a/Sample.Web.NetFramework/App_Start/FilterConfig.cs b/Sample.Web.NetFramework/App_Start/FilterConfig.cs new file mode 100644 index 00000000..983d3884 --- /dev/null +++ b/Sample.Web.NetFramework/App_Start/FilterConfig.cs @@ -0,0 +1,13 @@ +using System.Web; +using System.Web.Mvc; + +namespace Sample.Web.NetFramework +{ + public class FilterConfig + { + public static void RegisterGlobalFilters(GlobalFilterCollection filters) + { + filters.Add(new HandleErrorAttribute()); + } + } +} diff --git a/Sample.Web.NetFramework/App_Start/RouteConfig.cs b/Sample.Web.NetFramework/App_Start/RouteConfig.cs new file mode 100644 index 00000000..49e596cc --- /dev/null +++ b/Sample.Web.NetFramework/App_Start/RouteConfig.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; + +namespace Sample.Web.NetFramework +{ + public class RouteConfig + { + public static void RegisterRoutes(RouteCollection routes) + { + routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); + + routes.MapRoute( + name: "Default", + url: "{controller}/{action}/{id}", + defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } + ); + } + } +} diff --git a/Sample.Web.NetFramework/App_Start/WebApiConfig.cs b/Sample.Web.NetFramework/App_Start/WebApiConfig.cs new file mode 100644 index 00000000..a7e02e81 --- /dev/null +++ b/Sample.Web.NetFramework/App_Start/WebApiConfig.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Http; + +namespace Sample.Web.NetFramework +{ + public static class WebApiConfig + { + public static void Register(HttpConfiguration config) + { + // Web API configuration and services + + // Web API routes + config.MapHttpAttributeRoutes(); + + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional } + ); + } + } +} diff --git a/Sample.Web.NetFramework/Content/Site.css b/Sample.Web.NetFramework/Content/Site.css new file mode 100644 index 00000000..f80f5755 --- /dev/null +++ b/Sample.Web.NetFramework/Content/Site.css @@ -0,0 +1,30 @@ +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Set padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Override the default bootstrap behavior where horizontal description lists + will truncate terms that are too long to fit in the left column +*/ +.dl-horizontal dt { + white-space: normal; +} + +/* Set width on the form input elements since they're 100% wide by default */ +input, +select, +textarea { + max-width: 280px; +} + +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #777; + border-color: #fff +} diff --git a/Sample.Web.NetFramework/Content/bootstrap-theme.css b/Sample.Web.NetFramework/Content/bootstrap-theme.css new file mode 100644 index 00000000..ea33f76a --- /dev/null +++ b/Sample.Web.NetFramework/Content/bootstrap-theme.css @@ -0,0 +1,587 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); +} +.btn-default:active, +.btn-primary:active, +.btn-success:active, +.btn-info:active, +.btn-warning:active, +.btn-danger:active, +.btn-default.active, +.btn-primary.active, +.btn-success.active, +.btn-info.active, +.btn-warning.active, +.btn-danger.active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-default.disabled, +.btn-primary.disabled, +.btn-success.disabled, +.btn-info.disabled, +.btn-warning.disabled, +.btn-danger.disabled, +.btn-default[disabled], +.btn-primary[disabled], +.btn-success[disabled], +.btn-info[disabled], +.btn-warning[disabled], +.btn-danger[disabled], +fieldset[disabled] .btn-default, +fieldset[disabled] .btn-primary, +fieldset[disabled] .btn-success, +fieldset[disabled] .btn-info, +fieldset[disabled] .btn-warning, +fieldset[disabled] .btn-danger { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-default .badge, +.btn-primary .badge, +.btn-success .badge, +.btn-info .badge, +.btn-warning .badge, +.btn-danger .badge { + text-shadow: none; +} +.btn:active, +.btn.active { + background-image: none; +} +.btn-default { + background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); + background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #dbdbdb; + text-shadow: 0 1px 0 #fff; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus { + background-color: #e0e0e0; + background-position: 0 -15px; +} +.btn-default:active, +.btn-default.active { + background-color: #e0e0e0; + border-color: #dbdbdb; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #e0e0e0; + background-image: none; +} +.btn-primary { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); + background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #245580; +} +.btn-primary:hover, +.btn-primary:focus { + background-color: #265a88; + background-position: 0 -15px; +} +.btn-primary:active, +.btn-primary.active { + background-color: #265a88; + border-color: #245580; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #265a88; + background-image: none; +} +.btn-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); + background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #3e8f3e; +} +.btn-success:hover, +.btn-success:focus { + background-color: #419641; + background-position: 0 -15px; +} +.btn-success:active, +.btn-success.active { + background-color: #419641; + border-color: #3e8f3e; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #419641; + background-image: none; +} +.btn-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #28a4c9; +} +.btn-info:hover, +.btn-info:focus { + background-color: #2aabd2; + background-position: 0 -15px; +} +.btn-info:active, +.btn-info.active { + background-color: #2aabd2; + border-color: #28a4c9; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #2aabd2; + background-image: none; +} +.btn-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #e38d13; +} +.btn-warning:hover, +.btn-warning:focus { + background-color: #eb9316; + background-position: 0 -15px; +} +.btn-warning:active, +.btn-warning.active { + background-color: #eb9316; + border-color: #e38d13; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #eb9316; + background-image: none; +} +.btn-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); + background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #b92c28; +} +.btn-danger:hover, +.btn-danger:focus { + background-color: #c12e2a; + background-position: 0 -15px; +} +.btn-danger:active, +.btn-danger.active { + background-color: #c12e2a; + border-color: #b92c28; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #c12e2a; + background-image: none; +} +.thumbnail, +.img-thumbnail { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; + background-color: #e8e8e8; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; + background-color: #2e6da4; +} +.navbar-default { + background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); + background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8)); + background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); + background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); +} +.navbar-inverse { + background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); + background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border-radius: 4px; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); + background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); +} +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} +@media (max-width: 767px) { + .navbar .navbar-nav .open .dropdown-menu > .active > a, + .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; + } +} +.alert { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.alert-success { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); + background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); + background-repeat: repeat-x; + border-color: #b2dba1; +} +.alert-info { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); + background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); + background-repeat: repeat-x; + border-color: #9acfea; +} +.alert-warning { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); + background-repeat: repeat-x; + border-color: #f5e79e; +} +.alert-danger { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); + background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); + background-repeat: repeat-x; + border-color: #dca7a7; +} +.progress { + background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); + background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); + background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); + background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); + background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.list-group { + border-radius: 4px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 #286090; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); + background-repeat: repeat-x; + border-color: #2b669a; +} +.list-group-item.active .badge, +.list-group-item.active:hover .badge, +.list-group-item.active:focus .badge { + text-shadow: none; +} +.panel { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.panel-default > .panel-heading { + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; +} +.panel-primary > .panel-heading { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; +} +.panel-success > .panel-heading { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); + background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); + background-repeat: repeat-x; +} +.panel-info > .panel-heading { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); + background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); + background-repeat: repeat-x; +} +.panel-warning > .panel-heading { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); + background-repeat: repeat-x; +} +.panel-danger > .panel-heading { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); + background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); + background-repeat: repeat-x; +} +.well { + background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); + background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; + border-color: #dcdcdc; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); +} +/*# sourceMappingURL=bootstrap-theme.css.map */ \ No newline at end of file diff --git a/Sample.Web.NetFramework/Content/bootstrap-theme.css.map b/Sample.Web.NetFramework/Content/bootstrap-theme.css.map new file mode 100644 index 00000000..949d0973 --- /dev/null +++ b/Sample.Web.NetFramework/Content/bootstrap-theme.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap-theme.css","less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAAA;;;;GAIG;ACiBH;;;;;;EAME,yCAAA;EC2CA,4FAAA;EACQ,oFAAA;CFzDT;ACkBC;;;;;;;;;;;;ECsCA,yDAAA;EACQ,iDAAA;CF1CT;ACQC;;;;;;;;;;;;;;;;;;ECiCA,yBAAA;EACQ,iBAAA;CFrBT;AC7BD;;;;;;EAuBI,kBAAA;CDcH;AC2BC;;EAEE,uBAAA;CDzBH;AC8BD;EEvEI,sEAAA;EACA,iEAAA;EACA,2FAAA;EAAA,oEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;EAyCA,0BAAA;EACA,mBAAA;CDtBD;AClBC;;EAEE,0BAAA;EACA,6BAAA;CDoBH;ACjBC;;EAEE,0BAAA;EACA,sBAAA;CDmBH;ACbG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD2BL;ACPD;EE5EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CD4DD;AC1DC;;EAEE,0BAAA;EACA,6BAAA;CD4DH;ACzDC;;EAEE,0BAAA;EACA,sBAAA;CD2DH;ACrDG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDmEL;AC9CD;EE7EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CDoGD;AClGC;;EAEE,0BAAA;EACA,6BAAA;CDoGH;ACjGC;;EAEE,0BAAA;EACA,sBAAA;CDmGH;AC7FG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD2GL;ACrFD;EE9EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CD4ID;AC1IC;;EAEE,0BAAA;EACA,6BAAA;CD4IH;ACzIC;;EAEE,0BAAA;EACA,sBAAA;CD2IH;ACrIG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDmJL;AC5HD;EE/EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CDoLD;AClLC;;EAEE,0BAAA;EACA,6BAAA;CDoLH;ACjLC;;EAEE,0BAAA;EACA,sBAAA;CDmLH;AC7KG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD2LL;ACnKD;EEhFI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CD4ND;AC1NC;;EAEE,0BAAA;EACA,6BAAA;CD4NH;ACzNC;;EAEE,0BAAA;EACA,sBAAA;CD2NH;ACrNG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDmOL;ACpMD;;ECtCE,mDAAA;EACQ,2CAAA;CF8OT;AC/LD;;EEjGI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFgGF,0BAAA;CDqMD;ACnMD;;;EEtGI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFsGF,0BAAA;CDyMD;AChMD;EEnHI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;ECnBF,oEAAA;EHqIA,mBAAA;ECrEA,4FAAA;EACQ,oFAAA;CF4QT;AC3MD;;EEnHI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;ED6CF,yDAAA;EACQ,iDAAA;CFsRT;ACxMD;;EAEE,+CAAA;CD0MD;ACtMD;EEtII,sEAAA;EACA,iEAAA;EACA,2FAAA;EAAA,oEAAA;EACA,uHAAA;EACA,4BAAA;ECnBF,oEAAA;EHwJA,mBAAA;CD4MD;AC/MD;;EEtII,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;ED6CF,wDAAA;EACQ,gDAAA;CF6ST;ACzND;;EAYI,0CAAA;CDiNH;AC5MD;;;EAGE,iBAAA;CD8MD;AC1MD;EAEI;;;IAGE,YAAA;IEnKF,yEAAA;IACA,oEAAA;IACA,8FAAA;IAAA,uEAAA;IACA,uHAAA;IACA,4BAAA;GH+WD;CACF;ACrMD;EACE,8CAAA;EC/HA,2FAAA;EACQ,mFAAA;CFuUT;AC7LD;EE5LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDyMD;ACpMD;EE7LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDiND;AC3MD;EE9LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDyND;AClND;EE/LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDiOD;AClND;EEvMI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH4ZH;AC/MD;EEjNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHmaH;ACrND;EElNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH0aH;AC3ND;EEnNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHibH;ACjOD;EEpNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHwbH;ACvOD;EErNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH+bH;AC1OD;EExLI,8MAAA;EACA,yMAAA;EACA,sMAAA;CHqaH;ACtOD;EACE,mBAAA;EClLA,mDAAA;EACQ,2CAAA;CF2ZT;ACvOD;;;EAGE,8BAAA;EEzOE,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFuOF,sBAAA;CD6OD;AClPD;;;EAQI,kBAAA;CD+OH;ACrOD;ECvME,kDAAA;EACQ,0CAAA;CF+aT;AC/ND;EElQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHoeH;ACrOD;EEnQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH2eH;AC3OD;EEpQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHkfH;ACjPD;EErQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHyfH;ACvPD;EEtQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHggBH;AC7PD;EEvQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHugBH;AC7PD;EE9QI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EF4QF,sBAAA;EC/NA,0FAAA;EACQ,kFAAA;CFmeT","file":"bootstrap-theme.css","sourcesContent":["/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n background-repeat: repeat-x;\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n background-repeat: repeat-x;\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n background-repeat: repeat-x;\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","// stylelint-disable selector-no-qualifying-type, selector-max-compound-selectors\n\n/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0, 0, 0, .125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default {\n .btn-styles(@btn-default-bg);\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n .box-shadow(@shadow);\n}\n","// stylelint-disable indentation, property-no-vendor-prefix, selector-no-vendor-prefix\n\n// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n word-wrap: break-word;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// stylelint-disable value-no-vendor-prefix, selector-max-id\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255, 255, 255, .15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/Sample.Web.NetFramework/Content/bootstrap-theme.min.css b/Sample.Web.NetFramework/Content/bootstrap-theme.min.css new file mode 100644 index 00000000..2a69f48c --- /dev/null +++ b/Sample.Web.NetFramework/Content/bootstrap-theme.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x;background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x;background-color:#2e6da4}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} +/*# sourceMappingURL=bootstrap-theme.min.css.map */ \ No newline at end of file diff --git a/Sample.Web.NetFramework/Content/bootstrap-theme.min.css.map b/Sample.Web.NetFramework/Content/bootstrap-theme.min.css.map new file mode 100644 index 00000000..5d75106e --- /dev/null +++ b/Sample.Web.NetFramework/Content/bootstrap-theme.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap-theme.css","dist/css/bootstrap-theme.css","less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAAA;;;;ACUA,YCWA,aDbA,UAFA,aACA,aAEA,aCkBE,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBF7CV,mBANA,mBACA,oBCWE,oBDRF,iBANA,iBAIA,oBANA,oBAOA,oBANA,oBAQA,oBANA,oBEmDE,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBFpCV,qBAMA,sBCJE,sBDDF,uBAHA,mBAMA,oBARA,sBAMA,uBALA,sBAMA,uBAJA,sBAMA,uBAOA,+BALA,gCAGA,6BAFA,gCACA,gCAEA,gCEwBE,mBAAA,KACQ,WAAA,KFfV,mBCnCA,oBDiCA,iBAFA,oBACA,oBAEA,oBCXI,YAAA,KDgBJ,YCyBE,YAEE,iBAAA,KAKJ,aEvEI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QAyCA,YAAA,EAAA,IAAA,EAAA,KACA,aAAA,KDnBF,mBCrBE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MDuBJ,oBCpBE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBD8BJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCCdM,iBAAA,QACA,iBAAA,KAoBN,aE5EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDgEF,mBC9DE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MDgEJ,oBC7DE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBDuEJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCCvDM,iBAAA,QACA,iBAAA,KAqBN,aE7EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDyGF,mBCvGE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MDyGJ,oBCtGE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBDgHJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCChGM,iBAAA,QACA,iBAAA,KAsBN,UE9EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDkJF,gBChJE,gBAEE,iBAAA,QACA,oBAAA,EAAA,MDkJJ,iBC/IE,iBAEE,iBAAA,QACA,aAAA,QAMA,mBDyJJ,0BANA,yBAGA,0BANA,yBAHA,yBAFA,oBAeA,2BANA,0BAGA,2BANA,0BAHA,0BAFA,6BAeA,oCANA,mCAGA,oCANA,mCAHA,mCCzIM,iBAAA,QACA,iBAAA,KAuBN,aE/EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QD2LF,mBCzLE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MD2LJ,oBCxLE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBDkMJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCClLM,iBAAA,QACA,iBAAA,KAwBN,YEhFI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDoOF,kBClOE,kBAEE,iBAAA,QACA,oBAAA,EAAA,MDoOJ,mBCjOE,mBAEE,iBAAA,QACA,aAAA,QAMA,qBD2OJ,4BANA,2BAGA,4BANA,2BAHA,2BAFA,sBAeA,6BANA,4BAGA,6BANA,4BAHA,4BAFA,+BAeA,sCANA,qCAGA,sCANA,qCAHA,qCC3NM,iBAAA,QACA,iBAAA,KD2ON,eC5MA,WCtCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBFsPV,0BCvMA,0BEjGI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFgGF,iBAAA,QAEF,yBD6MA,+BADA,+BGlTI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsGF,iBAAA,QASF,gBEnHI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,kBAAA,SCnBF,OAAA,0DHqIA,cAAA,ICrEA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBFuRV,sCCtNA,oCEnHI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD6CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD8EV,cDoNA,iBClNE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEtII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,kBAAA,SCnBF,OAAA,0DHwJA,cAAA,IDyNF,sCC5NA,oCEtII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD6CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDoFV,8BDuOA,iCC3NI,YAAA,EAAA,KAAA,EAAA,gBDgOJ,qBADA,kBC1NA,mBAGE,cAAA,EAIF,yBAEI,mDDwNF,yDADA,yDCpNI,MAAA,KEnKF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UF2KJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC/HA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,gBD0IV,eE5LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAKF,YE7LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAMF,eE9LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAOF,cE/LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAeF,UEvMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6MJ,cEjNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8MJ,sBElNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,mBEnNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFgNJ,sBEpNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiNJ,qBErNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFqNJ,sBExLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKF+LJ,YACE,cAAA,IClLA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDoLV,wBDiQA,8BADA,8BC7PE,YAAA,EAAA,KAAA,EAAA,QEzOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuOF,aAAA,QALF,+BD6QA,qCADA,qCCpQI,YAAA,KAUJ,OCvME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBDgNV,8BElQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+PJ,8BEnQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFgQJ,8BEpQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiQJ,2BErQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFkQJ,8BEtQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFmQJ,6BEvQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0QJ,ME9QI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4QF,aAAA,QC/NA,mBAAA,MAAA,EAAA,IAAA,IAAA,eAAA,CAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,eAAA,CAAA,EAAA,IAAA,EAAA","sourcesContent":["/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n background-repeat: repeat-x;\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n background-repeat: repeat-x;\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n background-repeat: repeat-x;\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8));\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n background-repeat: repeat-x;\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n background-repeat: repeat-x;\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n background-repeat: repeat-x;\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","// stylelint-disable selector-no-qualifying-type, selector-max-compound-selectors\n\n/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0, 0, 0, .125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default {\n .btn-styles(@btn-default-bg);\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n .box-shadow(@shadow);\n}\n","// stylelint-disable indentation, property-no-vendor-prefix, selector-no-vendor-prefix\n\n// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n word-wrap: break-word;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// stylelint-disable value-no-vendor-prefix, selector-max-id\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255, 255, 255, .15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/Sample.Web.NetFramework/Content/bootstrap.css b/Sample.Web.NetFramework/Content/bootstrap.css new file mode 100644 index 00000000..fcab4155 --- /dev/null +++ b/Sample.Web.NetFramework/Content/bootstrap.css @@ -0,0 +1,6834 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + -moz-text-decoration: underline dotted; + text-decoration: underline dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +mark { + background: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: "Glyphicons Halflings"; + src: url("../fonts/glyphicons-halflings-regular.eot"); + src: url("../fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("../fonts/glyphicons-halflings-regular.woff2") format("woff2"), url("../fonts/glyphicons-halflings-regular.woff") format("woff"), url("../fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg"); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: "Glyphicons Halflings"; + font-style: normal; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: "\002a"; +} +.glyphicon-plus:before { + content: "\002b"; +} +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; +} +.glyphicon-minus:before { + content: "\2212"; +} +.glyphicon-cloud:before { + content: "\2601"; +} +.glyphicon-envelope:before { + content: "\2709"; +} +.glyphicon-pencil:before { + content: "\270f"; +} +.glyphicon-glass:before { + content: "\e001"; +} +.glyphicon-music:before { + content: "\e002"; +} +.glyphicon-search:before { + content: "\e003"; +} +.glyphicon-heart:before { + content: "\e005"; +} +.glyphicon-star:before { + content: "\e006"; +} +.glyphicon-star-empty:before { + content: "\e007"; +} +.glyphicon-user:before { + content: "\e008"; +} +.glyphicon-film:before { + content: "\e009"; +} +.glyphicon-th-large:before { + content: "\e010"; +} +.glyphicon-th:before { + content: "\e011"; +} +.glyphicon-th-list:before { + content: "\e012"; +} +.glyphicon-ok:before { + content: "\e013"; +} +.glyphicon-remove:before { + content: "\e014"; +} +.glyphicon-zoom-in:before { + content: "\e015"; +} +.glyphicon-zoom-out:before { + content: "\e016"; +} +.glyphicon-off:before { + content: "\e017"; +} +.glyphicon-signal:before { + content: "\e018"; +} +.glyphicon-cog:before { + content: "\e019"; +} +.glyphicon-trash:before { + content: "\e020"; +} +.glyphicon-home:before { + content: "\e021"; +} +.glyphicon-file:before { + content: "\e022"; +} +.glyphicon-time:before { + content: "\e023"; +} +.glyphicon-road:before { + content: "\e024"; +} +.glyphicon-download-alt:before { + content: "\e025"; +} +.glyphicon-download:before { + content: "\e026"; +} +.glyphicon-upload:before { + content: "\e027"; +} +.glyphicon-inbox:before { + content: "\e028"; +} +.glyphicon-play-circle:before { + content: "\e029"; +} +.glyphicon-repeat:before { + content: "\e030"; +} +.glyphicon-refresh:before { + content: "\e031"; +} +.glyphicon-list-alt:before { + content: "\e032"; +} +.glyphicon-lock:before { + content: "\e033"; +} +.glyphicon-flag:before { + content: "\e034"; +} +.glyphicon-headphones:before { + content: "\e035"; +} +.glyphicon-volume-off:before { + content: "\e036"; +} +.glyphicon-volume-down:before { + content: "\e037"; +} +.glyphicon-volume-up:before { + content: "\e038"; +} +.glyphicon-qrcode:before { + content: "\e039"; +} +.glyphicon-barcode:before { + content: "\e040"; +} +.glyphicon-tag:before { + content: "\e041"; +} +.glyphicon-tags:before { + content: "\e042"; +} +.glyphicon-book:before { + content: "\e043"; +} +.glyphicon-bookmark:before { + content: "\e044"; +} +.glyphicon-print:before { + content: "\e045"; +} +.glyphicon-camera:before { + content: "\e046"; +} +.glyphicon-font:before { + content: "\e047"; +} +.glyphicon-bold:before { + content: "\e048"; +} +.glyphicon-italic:before { + content: "\e049"; +} +.glyphicon-text-height:before { + content: "\e050"; +} +.glyphicon-text-width:before { + content: "\e051"; +} +.glyphicon-align-left:before { + content: "\e052"; +} +.glyphicon-align-center:before { + content: "\e053"; +} +.glyphicon-align-right:before { + content: "\e054"; +} +.glyphicon-align-justify:before { + content: "\e055"; +} +.glyphicon-list:before { + content: "\e056"; +} +.glyphicon-indent-left:before { + content: "\e057"; +} +.glyphicon-indent-right:before { + content: "\e058"; +} +.glyphicon-facetime-video:before { + content: "\e059"; +} +.glyphicon-picture:before { + content: "\e060"; +} +.glyphicon-map-marker:before { + content: "\e062"; +} +.glyphicon-adjust:before { + content: "\e063"; +} +.glyphicon-tint:before { + content: "\e064"; +} +.glyphicon-edit:before { + content: "\e065"; +} +.glyphicon-share:before { + content: "\e066"; +} +.glyphicon-check:before { + content: "\e067"; +} +.glyphicon-move:before { + content: "\e068"; +} +.glyphicon-step-backward:before { + content: "\e069"; +} +.glyphicon-fast-backward:before { + content: "\e070"; +} +.glyphicon-backward:before { + content: "\e071"; +} +.glyphicon-play:before { + content: "\e072"; +} +.glyphicon-pause:before { + content: "\e073"; +} +.glyphicon-stop:before { + content: "\e074"; +} +.glyphicon-forward:before { + content: "\e075"; +} +.glyphicon-fast-forward:before { + content: "\e076"; +} +.glyphicon-step-forward:before { + content: "\e077"; +} +.glyphicon-eject:before { + content: "\e078"; +} +.glyphicon-chevron-left:before { + content: "\e079"; +} +.glyphicon-chevron-right:before { + content: "\e080"; +} +.glyphicon-plus-sign:before { + content: "\e081"; +} +.glyphicon-minus-sign:before { + content: "\e082"; +} +.glyphicon-remove-sign:before { + content: "\e083"; +} +.glyphicon-ok-sign:before { + content: "\e084"; +} +.glyphicon-question-sign:before { + content: "\e085"; +} +.glyphicon-info-sign:before { + content: "\e086"; +} +.glyphicon-screenshot:before { + content: "\e087"; +} +.glyphicon-remove-circle:before { + content: "\e088"; +} +.glyphicon-ok-circle:before { + content: "\e089"; +} +.glyphicon-ban-circle:before { + content: "\e090"; +} +.glyphicon-arrow-left:before { + content: "\e091"; +} +.glyphicon-arrow-right:before { + content: "\e092"; +} +.glyphicon-arrow-up:before { + content: "\e093"; +} +.glyphicon-arrow-down:before { + content: "\e094"; +} +.glyphicon-share-alt:before { + content: "\e095"; +} +.glyphicon-resize-full:before { + content: "\e096"; +} +.glyphicon-resize-small:before { + content: "\e097"; +} +.glyphicon-exclamation-sign:before { + content: "\e101"; +} +.glyphicon-gift:before { + content: "\e102"; +} +.glyphicon-leaf:before { + content: "\e103"; +} +.glyphicon-fire:before { + content: "\e104"; +} +.glyphicon-eye-open:before { + content: "\e105"; +} +.glyphicon-eye-close:before { + content: "\e106"; +} +.glyphicon-warning-sign:before { + content: "\e107"; +} +.glyphicon-plane:before { + content: "\e108"; +} +.glyphicon-calendar:before { + content: "\e109"; +} +.glyphicon-random:before { + content: "\e110"; +} +.glyphicon-comment:before { + content: "\e111"; +} +.glyphicon-magnet:before { + content: "\e112"; +} +.glyphicon-chevron-up:before { + content: "\e113"; +} +.glyphicon-chevron-down:before { + content: "\e114"; +} +.glyphicon-retweet:before { + content: "\e115"; +} +.glyphicon-shopping-cart:before { + content: "\e116"; +} +.glyphicon-folder-close:before { + content: "\e117"; +} +.glyphicon-folder-open:before { + content: "\e118"; +} +.glyphicon-resize-vertical:before { + content: "\e119"; +} +.glyphicon-resize-horizontal:before { + content: "\e120"; +} +.glyphicon-hdd:before { + content: "\e121"; +} +.glyphicon-bullhorn:before { + content: "\e122"; +} +.glyphicon-bell:before { + content: "\e123"; +} +.glyphicon-certificate:before { + content: "\e124"; +} +.glyphicon-thumbs-up:before { + content: "\e125"; +} +.glyphicon-thumbs-down:before { + content: "\e126"; +} +.glyphicon-hand-right:before { + content: "\e127"; +} +.glyphicon-hand-left:before { + content: "\e128"; +} +.glyphicon-hand-up:before { + content: "\e129"; +} +.glyphicon-hand-down:before { + content: "\e130"; +} +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} +.glyphicon-globe:before { + content: "\e135"; +} +.glyphicon-wrench:before { + content: "\e136"; +} +.glyphicon-tasks:before { + content: "\e137"; +} +.glyphicon-filter:before { + content: "\e138"; +} +.glyphicon-briefcase:before { + content: "\e139"; +} +.glyphicon-fullscreen:before { + content: "\e140"; +} +.glyphicon-dashboard:before { + content: "\e141"; +} +.glyphicon-paperclip:before { + content: "\e142"; +} +.glyphicon-heart-empty:before { + content: "\e143"; +} +.glyphicon-link:before { + content: "\e144"; +} +.glyphicon-phone:before { + content: "\e145"; +} +.glyphicon-pushpin:before { + content: "\e146"; +} +.glyphicon-usd:before { + content: "\e148"; +} +.glyphicon-gbp:before { + content: "\e149"; +} +.glyphicon-sort:before { + content: "\e150"; +} +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} +.glyphicon-sort-by-order:before { + content: "\e153"; +} +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} +.glyphicon-unchecked:before { + content: "\e157"; +} +.glyphicon-expand:before { + content: "\e158"; +} +.glyphicon-collapse-down:before { + content: "\e159"; +} +.glyphicon-collapse-up:before { + content: "\e160"; +} +.glyphicon-log-in:before { + content: "\e161"; +} +.glyphicon-flash:before { + content: "\e162"; +} +.glyphicon-log-out:before { + content: "\e163"; +} +.glyphicon-new-window:before { + content: "\e164"; +} +.glyphicon-record:before { + content: "\e165"; +} +.glyphicon-save:before { + content: "\e166"; +} +.glyphicon-open:before { + content: "\e167"; +} +.glyphicon-saved:before { + content: "\e168"; +} +.glyphicon-import:before { + content: "\e169"; +} +.glyphicon-export:before { + content: "\e170"; +} +.glyphicon-send:before { + content: "\e171"; +} +.glyphicon-floppy-disk:before { + content: "\e172"; +} +.glyphicon-floppy-saved:before { + content: "\e173"; +} +.glyphicon-floppy-remove:before { + content: "\e174"; +} +.glyphicon-floppy-save:before { + content: "\e175"; +} +.glyphicon-floppy-open:before { + content: "\e176"; +} +.glyphicon-credit-card:before { + content: "\e177"; +} +.glyphicon-transfer:before { + content: "\e178"; +} +.glyphicon-cutlery:before { + content: "\e179"; +} +.glyphicon-header:before { + content: "\e180"; +} +.glyphicon-compressed:before { + content: "\e181"; +} +.glyphicon-earphone:before { + content: "\e182"; +} +.glyphicon-phone-alt:before { + content: "\e183"; +} +.glyphicon-tower:before { + content: "\e184"; +} +.glyphicon-stats:before { + content: "\e185"; +} +.glyphicon-sd-video:before { + content: "\e186"; +} +.glyphicon-hd-video:before { + content: "\e187"; +} +.glyphicon-subtitles:before { + content: "\e188"; +} +.glyphicon-sound-stereo:before { + content: "\e189"; +} +.glyphicon-sound-dolby:before { + content: "\e190"; +} +.glyphicon-sound-5-1:before { + content: "\e191"; +} +.glyphicon-sound-6-1:before { + content: "\e192"; +} +.glyphicon-sound-7-1:before { + content: "\e193"; +} +.glyphicon-copyright-mark:before { + content: "\e194"; +} +.glyphicon-registration-mark:before { + content: "\e195"; +} +.glyphicon-cloud-download:before { + content: "\e197"; +} +.glyphicon-cloud-upload:before { + content: "\e198"; +} +.glyphicon-tree-conifer:before { + content: "\e199"; +} +.glyphicon-tree-deciduous:before { + content: "\e200"; +} +.glyphicon-cd:before { + content: "\e201"; +} +.glyphicon-save-file:before { + content: "\e202"; +} +.glyphicon-open-file:before { + content: "\e203"; +} +.glyphicon-level-up:before { + content: "\e204"; +} +.glyphicon-copy:before { + content: "\e205"; +} +.glyphicon-paste:before { + content: "\e206"; +} +.glyphicon-alert:before { + content: "\e209"; +} +.glyphicon-equalizer:before { + content: "\e210"; +} +.glyphicon-king:before { + content: "\e211"; +} +.glyphicon-queen:before { + content: "\e212"; +} +.glyphicon-pawn:before { + content: "\e213"; +} +.glyphicon-bishop:before { + content: "\e214"; +} +.glyphicon-knight:before { + content: "\e215"; +} +.glyphicon-baby-formula:before { + content: "\e216"; +} +.glyphicon-tent:before { + content: "\26fa"; +} +.glyphicon-blackboard:before { + content: "\e218"; +} +.glyphicon-bed:before { + content: "\e219"; +} +.glyphicon-apple:before { + content: "\f8ff"; +} +.glyphicon-erase:before { + content: "\e221"; +} +.glyphicon-hourglass:before { + content: "\231b"; +} +.glyphicon-lamp:before { + content: "\e223"; +} +.glyphicon-duplicate:before { + content: "\e224"; +} +.glyphicon-piggy-bank:before { + content: "\e225"; +} +.glyphicon-scissors:before { + content: "\e226"; +} +.glyphicon-bitcoin:before { + content: "\e227"; +} +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} +.glyphicon-yen:before { + content: "\00a5"; +} +.glyphicon-jpy:before { + content: "\00a5"; +} +.glyphicon-ruble:before { + content: "\20bd"; +} +.glyphicon-rub:before { + content: "\20bd"; +} +.glyphicon-scale:before { + content: "\e230"; +} +.glyphicon-ice-lolly:before { + content: "\e231"; +} +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} +.glyphicon-education:before { + content: "\e233"; +} +.glyphicon-option-horizontal:before { + content: "\e234"; +} +.glyphicon-option-vertical:before { + content: "\e235"; +} +.glyphicon-menu-hamburger:before { + content: "\e236"; +} +.glyphicon-modal-window:before { + content: "\e237"; +} +.glyphicon-oil:before { + content: "\e238"; +} +.glyphicon-grain:before { + content: "\e239"; +} +.glyphicon-sunglasses:before { + content: "\e240"; +} +.glyphicon-text-size:before { + content: "\e241"; +} +.glyphicon-text-color:before { + content: "\e242"; +} +.glyphicon-text-background:before { + content: "\e243"; +} +.glyphicon-object-align-top:before { + content: "\e244"; +} +.glyphicon-object-align-bottom:before { + content: "\e245"; +} +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} +.glyphicon-object-align-left:before { + content: "\e247"; +} +.glyphicon-object-align-vertical:before { + content: "\e248"; +} +.glyphicon-object-align-right:before { + content: "\e249"; +} +.glyphicon-triangle-right:before { + content: "\e250"; +} +.glyphicon-triangle-left:before { + content: "\e251"; +} +.glyphicon-triangle-bottom:before { + content: "\e252"; +} +.glyphicon-triangle-top:before { + content: "\e253"; +} +.glyphicon-console:before { + content: "\e254"; +} +.glyphicon-superscript:before { + content: "\e255"; +} +.glyphicon-subscript:before { + content: "\e256"; +} +.glyphicon-menu-left:before { + content: "\e257"; +} +.glyphicon-menu-right:before { + content: "\e258"; +} +.glyphicon-menu-down:before { + content: "\e259"; +} +.glyphicon-menu-up:before { + content: "\e260"; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role="button"] { + cursor: pointer; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: 400; + line-height: 1; + color: #777777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover, +a.text-primary:focus { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover, +a.text-info:focus { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + list-style: none; + margin-left: -5px; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: 700; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: "\2014 \00A0"; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eeeeee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ""; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: "\00A0 \2014"; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.row-no-gutters { + margin-right: 0; + margin-left: 0; +} +.row-no-gutters [class*="col-"] { + padding-right: 0; + padding-left: 0; +} +.col-xs-1, +.col-sm-1, +.col-md-1, +.col-lg-1, +.col-xs-2, +.col-sm-2, +.col-md-2, +.col-lg-2, +.col-xs-3, +.col-sm-3, +.col-md-3, +.col-lg-3, +.col-xs-4, +.col-sm-4, +.col-md-4, +.col-lg-4, +.col-xs-5, +.col-sm-5, +.col-md-5, +.col-lg-5, +.col-xs-6, +.col-sm-6, +.col-md-6, +.col-lg-6, +.col-xs-7, +.col-sm-7, +.col-md-7, +.col-lg-7, +.col-xs-8, +.col-sm-8, +.col-md-8, +.col-lg-8, +.col-xs-9, +.col-sm-9, +.col-md-9, +.col-lg-9, +.col-xs-10, +.col-sm-10, +.col-md-10, +.col-lg-10, +.col-xs-11, +.col-sm-11, +.col-md-11, +.col-lg-11, +.col-xs-12, +.col-sm-12, +.col-md-12, +.col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11, +.col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0%; +} +@media (min-width: 768px) { + .col-sm-1, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11, + .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 992px) { + .col-md-1, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11, + .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 1200px) { + .col-lg-1, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11, + .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0%; + } +} +table { + background-color: transparent; +} +table col[class*="col-"] { + position: static; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + min-height: 0.01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: 700; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eeeeee; + opacity: 1; +} +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} +textarea.form-control { + height: auto; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"].form-control, + input[type="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + vertical-align: middle; + cursor: pointer; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +.form-control-static { + min-height: 34px; + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.form-group-lg select.form-control { + height: 46px; + line-height: 46px; +} +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 11px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 11px; + font-size: 18px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + font-size: 12px; + } +} +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + filter: alpha(opacity=65); + opacity: 0.65; + -webkit-box-shadow: none; + box-shadow: none; +} +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + background-image: none; + border-color: #adadad; +} +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + background-image: none; + border-color: #204d74; +} +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus { + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + background-image: none; + border-color: #398439; +} +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + background-image: none; + border-color: #269abc; +} +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + background-image: none; + border-color: #d58512; +} +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + background-image: none; + border-color: #ac2925; +} +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: 400; + color: #337ab7; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; + -webkit-transition-duration: 0.35s; + -o-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropup, +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: 400; + line-height: 1.42857143; + color: #333333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group .form-control:focus { + z-index: 3; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: 400; + line-height: 1; + color: #555555; + text-align: center; + background-color: #eeeeee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + z-index: 2; + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} +.nav > li.disabled > a { + color: #777777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eeeeee; + border-color: #337ab7; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #337ab7; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-right: 15px; + margin-top: 8px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-right: -15px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + margin-top: 8px; + margin-bottom: 8px; +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #777777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + z-index: 2; + color: #23527c; + background-color: #eeeeee; + border-color: #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 3; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: #777777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eeeeee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + padding-right: 15px; + padding-left: 15px; + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border 0.2s ease-in-out; + -o-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + overflow: hidden; + zoom: 1; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-object.img-thumbnail { + max-width: none; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777777; + cursor: not-allowed; + background-color: #eeeeee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; +} +a.list-group-item, +button.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +button.list-group-item { + width: 100%; + text-align: left; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +button.list-group-item-success.active, +a.list-group-item-success.active:hover, +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +button.list-group-item-info.active, +a.list-group-item-info.active:hover, +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +button.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +button.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #337ab7; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: 0.2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: 0.5; +} +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out, -o-transform 0.3s ease-out; +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + outline: 0; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: 0.5; +} +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-style: normal; + font-weight: 400; + line-height: 1.42857143; + line-break: auto; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + font-size: 12px; + filter: alpha(opacity=0); + opacity: 0; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: 0.9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-style: normal; + font-weight: 400; + line-height: 1.42857143; + line-break: auto; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + font-size: 14px; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999999; + border-bottom-color: rgba(0, 0, 0, 0.25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + -o-transition: -o-transform 0.6s ease-in-out; + transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out, -o-transform 0.6s ease-in-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + left: 0; + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + left: 0; + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + left: 0; + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + background-color: rgba(0, 0, 0, 0); + filter: alpha(opacity=50); + opacity: 0.5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + outline: 0; + filter: alpha(opacity=90); + opacity: 0.9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -10px; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + font-family: serif; + line-height: 1; +} +.carousel-control .icon-prev:before { + content: "\2039"; +} +.carousel-control .icon-next:before { + content: "\203a"; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -10px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -10px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -10px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-header:before, +.modal-header:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-header:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/Sample.Web.NetFramework/Content/bootstrap.css.map b/Sample.Web.NetFramework/Content/bootstrap.css.map new file mode 100644 index 00000000..caac3e61 --- /dev/null +++ b/Sample.Web.NetFramework/Content/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,4EAA4E;ACK5E;EACE,wBAAA;EACA,2BAAA;EACA,+BAAA;CDHD;ACUD;EACE,UAAA;CDRD;ACqBD;;;;;;;;;;;;;EAaE,eAAA;CDnBD;AC2BD;;;;EAIE,sBAAA;EACA,yBAAA;CDzBD;ACiCD;EACE,cAAA;EACA,UAAA;CD/BD;ACuCD;;EAEE,cAAA;CDrCD;AC+CD;EACE,8BAAA;CD7CD;ACqDD;;EAEE,WAAA;CDnDD;AC8DD;EACE,oBAAA;EACA,2BAAA;EACA,0CAAA;EAAA,uCAAA;EAAA,kCAAA;CD5DD;ACmED;;EAEE,kBAAA;CDjED;ACwED;EACE,mBAAA;CDtED;AC8ED;EACE,eAAA;EACA,iBAAA;CD5ED;ACmFD;EACE,iBAAA;EACA,YAAA;CDjFD;ACwFD;EACE,eAAA;CDtFD;AC6FD;;EAEE,eAAA;EACA,eAAA;EACA,mBAAA;EACA,yBAAA;CD3FD;AC8FD;EACE,YAAA;CD5FD;AC+FD;EACE,gBAAA;CD7FD;ACuGD;EACE,UAAA;CDrGD;AC4GD;EACE,iBAAA;CD1GD;ACoHD;EACE,iBAAA;CDlHD;ACyHD;EACE,gCAAA;EAAA,6BAAA;EAAA,wBAAA;EACA,UAAA;CDvHD;AC8HD;EACE,eAAA;CD5HD;ACmID;;;;EAIE,kCAAA;EACA,eAAA;CDjID;ACmJD;;;;;EAKE,eAAA;EACA,cAAA;EACA,UAAA;CDjJD;ACwJD;EACE,kBAAA;CDtJD;ACgKD;;EAEE,qBAAA;CD9JD;ACyKD;;;;EAIE,2BAAA;EACA,gBAAA;CDvKD;AC8KD;;EAEE,gBAAA;CD5KD;ACmLD;;EAEE,UAAA;EACA,WAAA;CDjLD;ACyLD;EACE,oBAAA;CDvLD;ACkMD;;EAEE,+BAAA;EAAA,4BAAA;EAAA,uBAAA;EACA,WAAA;CDhMD;ACyMD;;EAEE,aAAA;CDvMD;AC+MD;EACE,8BAAA;EACA,gCAAA;EAAA,6BAAA;EAAA,wBAAA;CD7MD;ACsND;;EAEE,yBAAA;CDpND;AC2ND;EACE,0BAAA;EACA,cAAA;EACA,+BAAA;CDzND;ACiOD;EACE,UAAA;EACA,WAAA;CD/ND;ACsOD;EACE,eAAA;CDpOD;AC4OD;EACE,kBAAA;CD1OD;ACoPD;EACE,0BAAA;EACA,kBAAA;CDlPD;ACqPD;;EAEE,WAAA;CDnPD;AACD,qFAAqF;AEhLrF;EACE;;;IAGE,uBAAA;IACA,6BAAA;IACA,mCAAA;IACA,oCAAA;IAAA,4BAAA;GFkLD;EE/KD;;IAEE,2BAAA;GFiLD;EE9KD;IACE,6BAAA;GFgLD;EE7KD;IACE,8BAAA;GF+KD;EE1KD;;IAEE,YAAA;GF4KD;EEzKD;;IAEE,uBAAA;IACA,yBAAA;GF2KD;EExKD;IACE,4BAAA;GF0KD;EEvKD;;IAEE,yBAAA;GFyKD;EEtKD;IACE,2BAAA;GFwKD;EErKD;;;IAGE,WAAA;IACA,UAAA;GFuKD;EEpKD;;IAEE,wBAAA;GFsKD;EEhKD;IACE,cAAA;GFkKD;EEhKD;;IAGI,kCAAA;GFiKH;EE9JD;IACE,uBAAA;GFgKD;EE7JD;IACE,qCAAA;GF+JD;EEhKD;;IAKI,kCAAA;GF+JH;EE5JD;;IAGI,kCAAA;GF6JH;CACF;AGnPD;EACE,oCAAA;EACA,sDAAA;EACA,gYAAA;CHqPD;AG7OD;EACE,mBAAA;EACA,SAAA;EACA,sBAAA;EACA,oCAAA;EACA,mBAAA;EACA,iBAAA;EACA,eAAA;EACA,oCAAA;EACA,mCAAA;CH+OD;AG3OmC;EAAW,iBAAA;CH8O9C;AG7OmC;EAAW,iBAAA;CHgP9C;AG9OmC;;EAAW,iBAAA;CHkP9C;AGjPmC;EAAW,iBAAA;CHoP9C;AGnPmC;EAAW,iBAAA;CHsP9C;AGrPmC;EAAW,iBAAA;CHwP9C;AGvPmC;EAAW,iBAAA;CH0P9C;AGzPmC;EAAW,iBAAA;CH4P9C;AG3PmC;EAAW,iBAAA;CH8P9C;AG7PmC;EAAW,iBAAA;CHgQ9C;AG/PmC;EAAW,iBAAA;CHkQ9C;AGjQmC;EAAW,iBAAA;CHoQ9C;AGnQmC;EAAW,iBAAA;CHsQ9C;AGrQmC;EAAW,iBAAA;CHwQ9C;AGvQmC;EAAW,iBAAA;CH0Q9C;AGzQmC;EAAW,iBAAA;CH4Q9C;AG3QmC;EAAW,iBAAA;CH8Q9C;AG7QmC;EAAW,iBAAA;CHgR9C;AG/QmC;EAAW,iBAAA;CHkR9C;AGjRmC;EAAW,iBAAA;CHoR9C;AGnRmC;EAAW,iBAAA;CHsR9C;AGrRmC;EAAW,iBAAA;CHwR9C;AGvRmC;EAAW,iBAAA;CH0R9C;AGzRmC;EAAW,iBAAA;CH4R9C;AG3RmC;EAAW,iBAAA;CH8R9C;AG7RmC;EAAW,iBAAA;CHgS9C;AG/RmC;EAAW,iBAAA;CHkS9C;AGjSmC;EAAW,iBAAA;CHoS9C;AGnSmC;EAAW,iBAAA;CHsS9C;AGrSmC;EAAW,iBAAA;CHwS9C;AGvSmC;EAAW,iBAAA;CH0S9C;AGzSmC;EAAW,iBAAA;CH4S9C;AG3SmC;EAAW,iBAAA;CH8S9C;AG7SmC;EAAW,iBAAA;CHgT9C;AG/SmC;EAAW,iBAAA;CHkT9C;AGjTmC;EAAW,iBAAA;CHoT9C;AGnTmC;EAAW,iBAAA;CHsT9C;AGrTmC;EAAW,iBAAA;CHwT9C;AGvTmC;EAAW,iBAAA;CH0T9C;AGzTmC;EAAW,iBAAA;CH4T9C;AG3TmC;EAAW,iBAAA;CH8T9C;AG7TmC;EAAW,iBAAA;CHgU9C;AG/TmC;EAAW,iBAAA;CHkU9C;AGjUmC;EAAW,iBAAA;CHoU9C;AGnUmC;EAAW,iBAAA;CHsU9C;AGrUmC;EAAW,iBAAA;CHwU9C;AGvUmC;EAAW,iBAAA;CH0U9C;AGzUmC;EAAW,iBAAA;CH4U9C;AG3UmC;EAAW,iBAAA;CH8U9C;AG7UmC;EAAW,iBAAA;CHgV9C;AG/UmC;EAAW,iBAAA;CHkV9C;AGjVmC;EAAW,iBAAA;CHoV9C;AGnVmC;EAAW,iBAAA;CHsV9C;AGrVmC;EAAW,iBAAA;CHwV9C;AGvVmC;EAAW,iBAAA;CH0V9C;AGzVmC;EAAW,iBAAA;CH4V9C;AG3VmC;EAAW,iBAAA;CH8V9C;AG7VmC;EAAW,iBAAA;CHgW9C;AG/VmC;EAAW,iBAAA;CHkW9C;AGjWmC;EAAW,iBAAA;CHoW9C;AGnWmC;EAAW,iBAAA;CHsW9C;AGrWmC;EAAW,iBAAA;CHwW9C;AGvWmC;EAAW,iBAAA;CH0W9C;AGzWmC;EAAW,iBAAA;CH4W9C;AG3WmC;EAAW,iBAAA;CH8W9C;AG7WmC;EAAW,iBAAA;CHgX9C;AG/WmC;EAAW,iBAAA;CHkX9C;AGjXmC;EAAW,iBAAA;CHoX9C;AGnXmC;EAAW,iBAAA;CHsX9C;AGrXmC;EAAW,iBAAA;CHwX9C;AGvXmC;EAAW,iBAAA;CH0X9C;AGzXmC;EAAW,iBAAA;CH4X9C;AG3XmC;EAAW,iBAAA;CH8X9C;AG7XmC;EAAW,iBAAA;CHgY9C;AG/XmC;EAAW,iBAAA;CHkY9C;AGjYmC;EAAW,iBAAA;CHoY9C;AGnYmC;EAAW,iBAAA;CHsY9C;AGrYmC;EAAW,iBAAA;CHwY9C;AGvYmC;EAAW,iBAAA;CH0Y9C;AGzYmC;EAAW,iBAAA;CH4Y9C;AG3YmC;EAAW,iBAAA;CH8Y9C;AG7YmC;EAAW,iBAAA;CHgZ9C;AG/YmC;EAAW,iBAAA;CHkZ9C;AGjZmC;EAAW,iBAAA;CHoZ9C;AGnZmC;EAAW,iBAAA;CHsZ9C;AGrZmC;EAAW,iBAAA;CHwZ9C;AGvZmC;EAAW,iBAAA;CH0Z9C;AGzZmC;EAAW,iBAAA;CH4Z9C;AG3ZmC;EAAW,iBAAA;CH8Z9C;AG7ZmC;EAAW,iBAAA;CHga9C;AG/ZmC;EAAW,iBAAA;CHka9C;AGjamC;EAAW,iBAAA;CHoa9C;AGnamC;EAAW,iBAAA;CHsa9C;AGramC;EAAW,iBAAA;CHwa9C;AGvamC;EAAW,iBAAA;CH0a9C;AGzamC;EAAW,iBAAA;CH4a9C;AG3amC;EAAW,iBAAA;CH8a9C;AG7amC;EAAW,iBAAA;CHgb9C;AG/amC;EAAW,iBAAA;CHkb9C;AGjbmC;EAAW,iBAAA;CHob9C;AGnbmC;EAAW,iBAAA;CHsb9C;AGrbmC;EAAW,iBAAA;CHwb9C;AGvbmC;EAAW,iBAAA;CH0b9C;AGzbmC;EAAW,iBAAA;CH4b9C;AG3bmC;EAAW,iBAAA;CH8b9C;AG7bmC;EAAW,iBAAA;CHgc9C;AG/bmC;EAAW,iBAAA;CHkc9C;AGjcmC;EAAW,iBAAA;CHoc9C;AGncmC;EAAW,iBAAA;CHsc9C;AGrcmC;EAAW,iBAAA;CHwc9C;AGvcmC;EAAW,iBAAA;CH0c9C;AGzcmC;EAAW,iBAAA;CH4c9C;AG3cmC;EAAW,iBAAA;CH8c9C;AG7cmC;EAAW,iBAAA;CHgd9C;AG/cmC;EAAW,iBAAA;CHkd9C;AGjdmC;EAAW,iBAAA;CHod9C;AGndmC;EAAW,iBAAA;CHsd9C;AGrdmC;EAAW,iBAAA;CHwd9C;AGvdmC;EAAW,iBAAA;CH0d9C;AGzdmC;EAAW,iBAAA;CH4d9C;AG3dmC;EAAW,iBAAA;CH8d9C;AG7dmC;EAAW,iBAAA;CHge9C;AG/dmC;EAAW,iBAAA;CHke9C;AGjemC;EAAW,iBAAA;CHoe9C;AGnemC;EAAW,iBAAA;CHse9C;AGremC;EAAW,iBAAA;CHwe9C;AGvemC;EAAW,iBAAA;CH0e9C;AGzemC;EAAW,iBAAA;CH4e9C;AG3emC;EAAW,iBAAA;CH8e9C;AG7emC;EAAW,iBAAA;CHgf9C;AG/emC;EAAW,iBAAA;CHkf9C;AGjfmC;EAAW,iBAAA;CHof9C;AGnfmC;EAAW,iBAAA;CHsf9C;AGrfmC;EAAW,iBAAA;CHwf9C;AGvfmC;EAAW,iBAAA;CH0f9C;AGzfmC;EAAW,iBAAA;CH4f9C;AG3fmC;EAAW,iBAAA;CH8f9C;AG7fmC;EAAW,iBAAA;CHggB9C;AG/fmC;EAAW,iBAAA;CHkgB9C;AGjgBmC;EAAW,iBAAA;CHogB9C;AGngBmC;EAAW,iBAAA;CHsgB9C;AGrgBmC;EAAW,iBAAA;CHwgB9C;AGvgBmC;EAAW,iBAAA;CH0gB9C;AGzgBmC;EAAW,iBAAA;CH4gB9C;AG3gBmC;EAAW,iBAAA;CH8gB9C;AG7gBmC;EAAW,iBAAA;CHghB9C;AG/gBmC;EAAW,iBAAA;CHkhB9C;AGjhBmC;EAAW,iBAAA;CHohB9C;AGnhBmC;EAAW,iBAAA;CHshB9C;AGrhBmC;EAAW,iBAAA;CHwhB9C;AGvhBmC;EAAW,iBAAA;CH0hB9C;AGzhBmC;EAAW,iBAAA;CH4hB9C;AG3hBmC;EAAW,iBAAA;CH8hB9C;AG7hBmC;EAAW,iBAAA;CHgiB9C;AG/hBmC;EAAW,iBAAA;CHkiB9C;AGjiBmC;EAAW,iBAAA;CHoiB9C;AGniBmC;EAAW,iBAAA;CHsiB9C;AGriBmC;EAAW,iBAAA;CHwiB9C;AGviBmC;EAAW,iBAAA;CH0iB9C;AGziBmC;EAAW,iBAAA;CH4iB9C;AG3iBmC;EAAW,iBAAA;CH8iB9C;AG7iBmC;EAAW,iBAAA;CHgjB9C;AG/iBmC;EAAW,iBAAA;CHkjB9C;AGjjBmC;EAAW,iBAAA;CHojB9C;AGnjBmC;EAAW,iBAAA;CHsjB9C;AGrjBmC;EAAW,iBAAA;CHwjB9C;AGvjBmC;EAAW,iBAAA;CH0jB9C;AGzjBmC;EAAW,iBAAA;CH4jB9C;AG3jBmC;EAAW,iBAAA;CH8jB9C;AG7jBmC;EAAW,iBAAA;CHgkB9C;AG/jBmC;EAAW,iBAAA;CHkkB9C;AGjkBmC;EAAW,iBAAA;CHokB9C;AGnkBmC;EAAW,iBAAA;CHskB9C;AGrkBmC;EAAW,iBAAA;CHwkB9C;AGvkBmC;EAAW,iBAAA;CH0kB9C;AGzkBmC;EAAW,iBAAA;CH4kB9C;AG3kBmC;EAAW,iBAAA;CH8kB9C;AG7kBmC;EAAW,iBAAA;CHglB9C;AG/kBmC;EAAW,iBAAA;CHklB9C;AGjlBmC;EAAW,iBAAA;CHolB9C;AGnlBmC;EAAW,iBAAA;CHslB9C;AGrlBmC;EAAW,iBAAA;CHwlB9C;AGvlBmC;EAAW,iBAAA;CH0lB9C;AGzlBmC;EAAW,iBAAA;CH4lB9C;AG3lBmC;EAAW,iBAAA;CH8lB9C;AG7lBmC;EAAW,iBAAA;CHgmB9C;AG/lBmC;EAAW,iBAAA;CHkmB9C;AGjmBmC;EAAW,iBAAA;CHomB9C;AGnmBmC;EAAW,iBAAA;CHsmB9C;AGrmBmC;EAAW,iBAAA;CHwmB9C;AGvmBmC;EAAW,iBAAA;CH0mB9C;AGzmBmC;EAAW,iBAAA;CH4mB9C;AG3mBmC;EAAW,iBAAA;CH8mB9C;AG7mBmC;EAAW,iBAAA;CHgnB9C;AG/mBmC;EAAW,iBAAA;CHknB9C;AGjnBmC;EAAW,iBAAA;CHonB9C;AGnnBmC;EAAW,iBAAA;CHsnB9C;AGrnBmC;EAAW,iBAAA;CHwnB9C;AGvnBmC;EAAW,iBAAA;CH0nB9C;AGznBmC;EAAW,iBAAA;CH4nB9C;AG3nBmC;EAAW,iBAAA;CH8nB9C;AG7nBmC;EAAW,iBAAA;CHgoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AGvoBmC;EAAW,iBAAA;CH0oB9C;AGzoBmC;EAAW,iBAAA;CH4oB9C;AG3oBmC;EAAW,iBAAA;CH8oB9C;AG7oBmC;EAAW,iBAAA;CHgpB9C;AG/oBmC;EAAW,iBAAA;CHkpB9C;AGjpBmC;EAAW,iBAAA;CHopB9C;AGnpBmC;EAAW,iBAAA;CHspB9C;AGrpBmC;EAAW,iBAAA;CHwpB9C;AGvpBmC;EAAW,iBAAA;CH0pB9C;AGzpBmC;EAAW,iBAAA;CH4pB9C;AG3pBmC;EAAW,iBAAA;CH8pB9C;AG7pBmC;EAAW,iBAAA;CHgqB9C;AG/pBmC;EAAW,iBAAA;CHkqB9C;AGjqBmC;EAAW,iBAAA;CHoqB9C;AGnqBmC;EAAW,iBAAA;CHsqB9C;AGrqBmC;EAAW,iBAAA;CHwqB9C;AGvqBmC;EAAW,iBAAA;CH0qB9C;AGzqBmC;EAAW,iBAAA;CH4qB9C;AG3qBmC;EAAW,iBAAA;CH8qB9C;AG7qBmC;EAAW,iBAAA;CHgrB9C;AG/qBmC;EAAW,iBAAA;CHkrB9C;AGjrBmC;EAAW,iBAAA;CHorB9C;AGnrBmC;EAAW,iBAAA;CHsrB9C;AGrrBmC;EAAW,iBAAA;CHwrB9C;AGvrBmC;EAAW,iBAAA;CH0rB9C;AGzrBmC;EAAW,iBAAA;CH4rB9C;AG3rBmC;EAAW,iBAAA;CH8rB9C;AG7rBmC;EAAW,iBAAA;CHgsB9C;AG/rBmC;EAAW,iBAAA;CHksB9C;AGjsBmC;EAAW,iBAAA;CHosB9C;AGnsBmC;EAAW,iBAAA;CHssB9C;AGrsBmC;EAAW,iBAAA;CHwsB9C;AGvsBmC;EAAW,iBAAA;CH0sB9C;AGzsBmC;EAAW,iBAAA;CH4sB9C;AG3sBmC;EAAW,iBAAA;CH8sB9C;AG7sBmC;EAAW,iBAAA;CHgtB9C;AG/sBmC;EAAW,iBAAA;CHktB9C;AGjtBmC;EAAW,iBAAA;CHotB9C;AGntBmC;EAAW,iBAAA;CHstB9C;AGrtBmC;EAAW,iBAAA;CHwtB9C;AGvtBmC;EAAW,iBAAA;CH0tB9C;AGztBmC;EAAW,iBAAA;CH4tB9C;AG3tBmC;EAAW,iBAAA;CH8tB9C;AG7tBmC;EAAW,iBAAA;CHguB9C;AG/tBmC;EAAW,iBAAA;CHkuB9C;AGjuBmC;EAAW,iBAAA;CHouB9C;AGnuBmC;EAAW,iBAAA;CHsuB9C;AGruBmC;EAAW,iBAAA;CHwuB9C;AGvuBmC;EAAW,iBAAA;CH0uB9C;AGzuBmC;EAAW,iBAAA;CH4uB9C;AG3uBmC;EAAW,iBAAA;CH8uB9C;AG7uBmC;EAAW,iBAAA;CHgvB9C;AIxhCD;ECkEE,+BAAA;EACG,4BAAA;EACK,uBAAA;CLy9BT;AI1hCD;;EC+DE,+BAAA;EACG,4BAAA;EACK,uBAAA;CL+9BT;AIxhCD;EACE,gBAAA;EACA,8CAAA;CJ0hCD;AIvhCD;EACE,4DAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,uBAAA;CJyhCD;AIrhCD;;;;EAIE,qBAAA;EACA,mBAAA;EACA,qBAAA;CJuhCD;AIjhCD;EACE,eAAA;EACA,sBAAA;CJmhCD;AIjhCC;;EAEE,eAAA;EACA,2BAAA;CJmhCH;AIhhCC;EEnDA,2CAAA;EACA,qBAAA;CNskCD;AIzgCD;EACE,UAAA;CJ2gCD;AIrgCD;EACE,uBAAA;CJugCD;AIngCD;;;;;EG1EE,eAAA;EACA,gBAAA;EACA,aAAA;CPolCD;AIvgCD;EACE,mBAAA;CJygCD;AIngCD;EACE,aAAA;EACA,wBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;EC+FA,yCAAA;EACK,oCAAA;EACG,iCAAA;EE5LR,sBAAA;EACA,gBAAA;EACA,aAAA;CPomCD;AIngCD;EACE,mBAAA;CJqgCD;AI//BD;EACE,iBAAA;EACA,oBAAA;EACA,UAAA;EACA,8BAAA;CJigCD;AIz/BD;EACE,mBAAA;EACA,WAAA;EACA,YAAA;EACA,WAAA;EACA,aAAA;EACA,iBAAA;EACA,uBAAA;EACA,UAAA;CJ2/BD;AIn/BC;;EAEE,iBAAA;EACA,YAAA;EACA,aAAA;EACA,UAAA;EACA,kBAAA;EACA,WAAA;CJq/BH;AI1+BD;EACE,gBAAA;CJ4+BD;AQjoCD;;;;;;;;;;;;EAEE,qBAAA;EACA,iBAAA;EACA,iBAAA;EACA,eAAA;CR6oCD;AQlpCD;;;;;;;;;;;;;;;;;;;;;;;;EASI,iBAAA;EACA,eAAA;EACA,eAAA;CRmqCH;AQ/pCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRoqCD;AQxqCD;;;;;;;;;;;;EAQI,eAAA;CR8qCH;AQ3qCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRgrCD;AQprCD;;;;;;;;;;;;EAQI,eAAA;CR0rCH;AQtrCD;;EAAU,gBAAA;CR0rCT;AQzrCD;;EAAU,gBAAA;CR6rCT;AQ5rCD;;EAAU,gBAAA;CRgsCT;AQ/rCD;;EAAU,gBAAA;CRmsCT;AQlsCD;;EAAU,gBAAA;CRssCT;AQrsCD;;EAAU,gBAAA;CRysCT;AQnsCD;EACE,iBAAA;CRqsCD;AQlsCD;EACE,oBAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;CRosCD;AQlsCC;EAAA;IACE,gBAAA;GRqsCD;CACF;AQ7rCD;;EAEE,eAAA;CR+rCD;AQ5rCD;;EAEE,eAAA;EACA,0BAAA;CR8rCD;AQ1rCD;EAAuB,iBAAA;CR6rCtB;AQ5rCD;EAAuB,kBAAA;CR+rCtB;AQ9rCD;EAAuB,mBAAA;CRisCtB;AQhsCD;EAAuB,oBAAA;CRmsCtB;AQlsCD;EAAuB,oBAAA;CRqsCtB;AQlsCD;EAAuB,0BAAA;CRqsCtB;AQpsCD;EAAuB,0BAAA;CRusCtB;AQtsCD;EAAuB,2BAAA;CRysCtB;AQtsCD;EACE,eAAA;CRwsCD;AQtsCD;ECvGE,eAAA;CTgzCD;AS/yCC;;EAEE,eAAA;CTizCH;AQ1sCD;EC1GE,eAAA;CTuzCD;AStzCC;;EAEE,eAAA;CTwzCH;AQ9sCD;EC7GE,eAAA;CT8zCD;AS7zCC;;EAEE,eAAA;CT+zCH;AQltCD;EChHE,eAAA;CTq0CD;ASp0CC;;EAEE,eAAA;CTs0CH;AQttCD;ECnHE,eAAA;CT40CD;AS30CC;;EAEE,eAAA;CT60CH;AQttCD;EAGE,YAAA;EE7HA,0BAAA;CVo1CD;AUn1CC;;EAEE,0BAAA;CVq1CH;AQxtCD;EEhIE,0BAAA;CV21CD;AU11CC;;EAEE,0BAAA;CV41CH;AQ5tCD;EEnIE,0BAAA;CVk2CD;AUj2CC;;EAEE,0BAAA;CVm2CH;AQhuCD;EEtIE,0BAAA;CVy2CD;AUx2CC;;EAEE,0BAAA;CV02CH;AQpuCD;EEzIE,0BAAA;CVg3CD;AU/2CC;;EAEE,0BAAA;CVi3CH;AQnuCD;EACE,oBAAA;EACA,oBAAA;EACA,iCAAA;CRquCD;AQ7tCD;;EAEE,cAAA;EACA,oBAAA;CR+tCD;AQluCD;;;;EAMI,iBAAA;CRkuCH;AQ3tCD;EACE,gBAAA;EACA,iBAAA;CR6tCD;AQztCD;EALE,gBAAA;EACA,iBAAA;EAMA,kBAAA;CR4tCD;AQ9tCD;EAKI,sBAAA;EACA,mBAAA;EACA,kBAAA;CR4tCH;AQvtCD;EACE,cAAA;EACA,oBAAA;CRytCD;AQvtCD;;EAEE,wBAAA;CRytCD;AQvtCD;EACE,iBAAA;CRytCD;AQvtCD;EACE,eAAA;CRytCD;AQ5sCC;EAAA;IAEI,YAAA;IACA,aAAA;IACA,YAAA;IACA,kBAAA;IGxNJ,iBAAA;IACA,wBAAA;IACA,oBAAA;GXu6CC;EQttCD;IASI,mBAAA;GRgtCH;CACF;AQtsCD;;EAEE,aAAA;CRwsCD;AQrsCD;EACE,eAAA;EA9IqB,0BAAA;CRs1CtB;AQnsCD;EACE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,+BAAA;CRqsCD;AQhsCG;;;EACE,iBAAA;CRosCL;AQ9sCD;;;EAmBI,eAAA;EACA,eAAA;EACA,wBAAA;EACA,eAAA;CRgsCH;AQ9rCG;;;EACE,uBAAA;CRksCL;AQ1rCD;;EAEE,oBAAA;EACA,gBAAA;EACA,kBAAA;EACA,gCAAA;EACA,eAAA;CR4rCD;AQtrCG;;;;;;EAAW,YAAA;CR8rCd;AQ7rCG;;;;;;EACE,uBAAA;CRosCL;AQ9rCD;EACE,oBAAA;EACA,mBAAA;EACA,wBAAA;CRgsCD;AYx+CD;;;;EAIE,+DAAA;CZ0+CD;AYt+CD;EACE,iBAAA;EACA,eAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CZw+CD;AYp+CD;EACE,iBAAA;EACA,eAAA;EACA,YAAA;EACA,uBAAA;EACA,mBAAA;EACA,uDAAA;EAAA,+CAAA;CZs+CD;AY5+CD;EASI,WAAA;EACA,gBAAA;EACA,iBAAA;EACA,yBAAA;EAAA,iBAAA;CZs+CH;AYj+CD;EACE,eAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,sBAAA;EACA,sBAAA;EACA,0BAAA;EACA,uBAAA;EACA,mBAAA;CZm+CD;AY9+CD;EAeI,WAAA;EACA,mBAAA;EACA,eAAA;EACA,sBAAA;EACA,8BAAA;EACA,iBAAA;CZk+CH;AY79CD;EACE,kBAAA;EACA,mBAAA;CZ+9CD;AazhDD;ECHE,oBAAA;EACA,mBAAA;EACA,mBAAA;EACA,kBAAA;Cd+hDD;Aa5hDC;EAAA;IACE,aAAA;Gb+hDD;CACF;Aa9hDC;EAAA;IACE,aAAA;GbiiDD;CACF;AahiDC;EAAA;IACE,cAAA;GbmiDD;CACF;Aa1hDD;ECvBE,oBAAA;EACA,mBAAA;EACA,mBAAA;EACA,kBAAA;CdojDD;AavhDD;ECvBE,oBAAA;EACA,mBAAA;CdijDD;AavhDD;EACE,gBAAA;EACA,eAAA;CbyhDD;Aa3hDD;EAKI,iBAAA;EACA,gBAAA;CbyhDH;AczkDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECiBK,mBAAA;EAEA,gBAAA;EAEA,oBAAA;EACA,mBAAA;CfwmDL;Ac9nDA;;;;;;;;;;;;ECuCK,YAAA;CfqmDL;Ac5oDA;EC+CG,YAAA;CfgmDH;Ac/oDA;EC+CG,oBAAA;CfmmDH;AclpDA;EC+CG,oBAAA;CfsmDH;AcrpDA;EC+CG,WAAA;CfymDH;AcxpDA;EC+CG,oBAAA;Cf4mDH;Ac3pDA;EC+CG,oBAAA;Cf+mDH;Ac9pDA;EC+CG,WAAA;CfknDH;AcjqDA;EC+CG,oBAAA;CfqnDH;AcpqDA;EC+CG,oBAAA;CfwnDH;AcvqDA;EC+CG,WAAA;Cf2nDH;Ac1qDA;EC+CG,oBAAA;Cf8nDH;Ac7qDA;EC+CG,mBAAA;CfioDH;AchrDA;EC8DG,YAAA;CfqnDH;AcnrDA;EC8DG,oBAAA;CfwnDH;ActrDA;EC8DG,oBAAA;Cf2nDH;AczrDA;EC8DG,WAAA;Cf8nDH;Ac5rDA;EC8DG,oBAAA;CfioDH;Ac/rDA;EC8DG,oBAAA;CfooDH;AclsDA;EC8DG,WAAA;CfuoDH;AcrsDA;EC8DG,oBAAA;Cf0oDH;AcxsDA;EC8DG,oBAAA;Cf6oDH;Ac3sDA;EC8DG,WAAA;CfgpDH;Ac9sDA;EC8DG,oBAAA;CfmpDH;AcjtDA;EC8DG,mBAAA;CfspDH;AcptDA;ECmEG,YAAA;CfopDH;AcvtDA;ECoDG,WAAA;CfsqDH;Ac1tDA;ECoDG,mBAAA;CfyqDH;Ac7tDA;ECoDG,mBAAA;Cf4qDH;AchuDA;ECoDG,UAAA;Cf+qDH;AcnuDA;ECoDG,mBAAA;CfkrDH;ActuDA;ECoDG,mBAAA;CfqrDH;AczuDA;ECoDG,UAAA;CfwrDH;Ac5uDA;ECoDG,mBAAA;Cf2rDH;Ac/uDA;ECoDG,mBAAA;Cf8rDH;AclvDA;ECoDG,UAAA;CfisDH;AcrvDA;ECoDG,mBAAA;CfosDH;AcxvDA;ECoDG,kBAAA;CfusDH;Ac3vDA;ECyDG,WAAA;CfqsDH;Ac9vDA;ECwEG,kBAAA;CfyrDH;AcjwDA;ECwEG,0BAAA;Cf4rDH;AcpwDA;ECwEG,0BAAA;Cf+rDH;AcvwDA;ECwEG,iBAAA;CfksDH;Ac1wDA;ECwEG,0BAAA;CfqsDH;Ac7wDA;ECwEG,0BAAA;CfwsDH;AchxDA;ECwEG,iBAAA;Cf2sDH;AcnxDA;ECwEG,0BAAA;Cf8sDH;ActxDA;ECwEG,0BAAA;CfitDH;AczxDA;ECwEG,iBAAA;CfotDH;Ac5xDA;ECwEG,0BAAA;CfutDH;Ac/xDA;ECwEG,yBAAA;Cf0tDH;AclyDA;ECwEG,gBAAA;Cf6tDH;Aa5tDD;ECzEC;;;;;;;;;;;;ICuCK,YAAA;Gf6wDH;EcpzDF;IC+CG,YAAA;GfwwDD;EcvzDF;IC+CG,oBAAA;Gf2wDD;Ec1zDF;IC+CG,oBAAA;Gf8wDD;Ec7zDF;IC+CG,WAAA;GfixDD;Ech0DF;IC+CG,oBAAA;GfoxDD;Ecn0DF;IC+CG,oBAAA;GfuxDD;Ect0DF;IC+CG,WAAA;Gf0xDD;Ecz0DF;IC+CG,oBAAA;Gf6xDD;Ec50DF;IC+CG,oBAAA;GfgyDD;Ec/0DF;IC+CG,WAAA;GfmyDD;Ecl1DF;IC+CG,oBAAA;GfsyDD;Ecr1DF;IC+CG,mBAAA;GfyyDD;Ecx1DF;IC8DG,YAAA;Gf6xDD;Ec31DF;IC8DG,oBAAA;GfgyDD;Ec91DF;IC8DG,oBAAA;GfmyDD;Ecj2DF;IC8DG,WAAA;GfsyDD;Ecp2DF;IC8DG,oBAAA;GfyyDD;Ecv2DF;IC8DG,oBAAA;Gf4yDD;Ec12DF;IC8DG,WAAA;Gf+yDD;Ec72DF;IC8DG,oBAAA;GfkzDD;Ech3DF;IC8DG,oBAAA;GfqzDD;Ecn3DF;IC8DG,WAAA;GfwzDD;Ect3DF;IC8DG,oBAAA;Gf2zDD;Ecz3DF;IC8DG,mBAAA;Gf8zDD;Ec53DF;ICmEG,YAAA;Gf4zDD;Ec/3DF;ICoDG,WAAA;Gf80DD;Ecl4DF;ICoDG,mBAAA;Gfi1DD;Ecr4DF;ICoDG,mBAAA;Gfo1DD;Ecx4DF;ICoDG,UAAA;Gfu1DD;Ec34DF;ICoDG,mBAAA;Gf01DD;Ec94DF;ICoDG,mBAAA;Gf61DD;Ecj5DF;ICoDG,UAAA;Gfg2DD;Ecp5DF;ICoDG,mBAAA;Gfm2DD;Ecv5DF;ICoDG,mBAAA;Gfs2DD;Ec15DF;ICoDG,UAAA;Gfy2DD;Ec75DF;ICoDG,mBAAA;Gf42DD;Ech6DF;ICoDG,kBAAA;Gf+2DD;Ecn6DF;ICyDG,WAAA;Gf62DD;Ect6DF;ICwEG,kBAAA;Gfi2DD;Ecz6DF;ICwEG,0BAAA;Gfo2DD;Ec56DF;ICwEG,0BAAA;Gfu2DD;Ec/6DF;ICwEG,iBAAA;Gf02DD;Ecl7DF;ICwEG,0BAAA;Gf62DD;Ecr7DF;ICwEG,0BAAA;Gfg3DD;Ecx7DF;ICwEG,iBAAA;Gfm3DD;Ec37DF;ICwEG,0BAAA;Gfs3DD;Ec97DF;ICwEG,0BAAA;Gfy3DD;Ecj8DF;ICwEG,iBAAA;Gf43DD;Ecp8DF;ICwEG,0BAAA;Gf+3DD;Ecv8DF;ICwEG,yBAAA;Gfk4DD;Ec18DF;ICwEG,gBAAA;Gfq4DD;CACF;Aa53DD;EClFC;;;;;;;;;;;;ICuCK,YAAA;Gfs7DH;Ec79DF;IC+CG,YAAA;Gfi7DD;Ech+DF;IC+CG,oBAAA;Gfo7DD;Ecn+DF;IC+CG,oBAAA;Gfu7DD;Ect+DF;IC+CG,WAAA;Gf07DD;Ecz+DF;IC+CG,oBAAA;Gf67DD;Ec5+DF;IC+CG,oBAAA;Gfg8DD;Ec/+DF;IC+CG,WAAA;Gfm8DD;Ecl/DF;IC+CG,oBAAA;Gfs8DD;Ecr/DF;IC+CG,oBAAA;Gfy8DD;Ecx/DF;IC+CG,WAAA;Gf48DD;Ec3/DF;IC+CG,oBAAA;Gf+8DD;Ec9/DF;IC+CG,mBAAA;Gfk9DD;EcjgEF;IC8DG,YAAA;Gfs8DD;EcpgEF;IC8DG,oBAAA;Gfy8DD;EcvgEF;IC8DG,oBAAA;Gf48DD;Ec1gEF;IC8DG,WAAA;Gf+8DD;Ec7gEF;IC8DG,oBAAA;Gfk9DD;EchhEF;IC8DG,oBAAA;Gfq9DD;EcnhEF;IC8DG,WAAA;Gfw9DD;EcthEF;IC8DG,oBAAA;Gf29DD;EczhEF;IC8DG,oBAAA;Gf89DD;Ec5hEF;IC8DG,WAAA;Gfi+DD;Ec/hEF;IC8DG,oBAAA;Gfo+DD;EcliEF;IC8DG,mBAAA;Gfu+DD;EcriEF;ICmEG,YAAA;Gfq+DD;EcxiEF;ICoDG,WAAA;Gfu/DD;Ec3iEF;ICoDG,mBAAA;Gf0/DD;Ec9iEF;ICoDG,mBAAA;Gf6/DD;EcjjEF;ICoDG,UAAA;GfggED;EcpjEF;ICoDG,mBAAA;GfmgED;EcvjEF;ICoDG,mBAAA;GfsgED;Ec1jEF;ICoDG,UAAA;GfygED;Ec7jEF;ICoDG,mBAAA;Gf4gED;EchkEF;ICoDG,mBAAA;Gf+gED;EcnkEF;ICoDG,UAAA;GfkhED;EctkEF;ICoDG,mBAAA;GfqhED;EczkEF;ICoDG,kBAAA;GfwhED;Ec5kEF;ICyDG,WAAA;GfshED;Ec/kEF;ICwEG,kBAAA;Gf0gED;EcllEF;ICwEG,0BAAA;Gf6gED;EcrlEF;ICwEG,0BAAA;GfghED;EcxlEF;ICwEG,iBAAA;GfmhED;Ec3lEF;ICwEG,0BAAA;GfshED;Ec9lEF;ICwEG,0BAAA;GfyhED;EcjmEF;ICwEG,iBAAA;Gf4hED;EcpmEF;ICwEG,0BAAA;Gf+hED;EcvmEF;ICwEG,0BAAA;GfkiED;Ec1mEF;ICwEG,iBAAA;GfqiED;Ec7mEF;ICwEG,0BAAA;GfwiED;EchnEF;ICwEG,yBAAA;Gf2iED;EcnnEF;ICwEG,gBAAA;Gf8iED;CACF;Aa5hED;EC3FC;;;;;;;;;;;;ICuCK,YAAA;Gf+lEH;EctoEF;IC+CG,YAAA;Gf0lED;EczoEF;IC+CG,oBAAA;Gf6lED;Ec5oEF;IC+CG,oBAAA;GfgmED;Ec/oEF;IC+CG,WAAA;GfmmED;EclpEF;IC+CG,oBAAA;GfsmED;EcrpEF;IC+CG,oBAAA;GfymED;EcxpEF;IC+CG,WAAA;Gf4mED;Ec3pEF;IC+CG,oBAAA;Gf+mED;Ec9pEF;IC+CG,oBAAA;GfknED;EcjqEF;IC+CG,WAAA;GfqnED;EcpqEF;IC+CG,oBAAA;GfwnED;EcvqEF;IC+CG,mBAAA;Gf2nED;Ec1qEF;IC8DG,YAAA;Gf+mED;Ec7qEF;IC8DG,oBAAA;GfknED;EchrEF;IC8DG,oBAAA;GfqnED;EcnrEF;IC8DG,WAAA;GfwnED;EctrEF;IC8DG,oBAAA;Gf2nED;EczrEF;IC8DG,oBAAA;Gf8nED;Ec5rEF;IC8DG,WAAA;GfioED;Ec/rEF;IC8DG,oBAAA;GfooED;EclsEF;IC8DG,oBAAA;GfuoED;EcrsEF;IC8DG,WAAA;Gf0oED;EcxsEF;IC8DG,oBAAA;Gf6oED;Ec3sEF;IC8DG,mBAAA;GfgpED;Ec9sEF;ICmEG,YAAA;Gf8oED;EcjtEF;ICoDG,WAAA;GfgqED;EcptEF;ICoDG,mBAAA;GfmqED;EcvtEF;ICoDG,mBAAA;GfsqED;Ec1tEF;ICoDG,UAAA;GfyqED;Ec7tEF;ICoDG,mBAAA;Gf4qED;EchuEF;ICoDG,mBAAA;Gf+qED;EcnuEF;ICoDG,UAAA;GfkrED;EctuEF;ICoDG,mBAAA;GfqrED;EczuEF;ICoDG,mBAAA;GfwrED;Ec5uEF;ICoDG,UAAA;Gf2rED;Ec/uEF;ICoDG,mBAAA;Gf8rED;EclvEF;ICoDG,kBAAA;GfisED;EcrvEF;ICyDG,WAAA;Gf+rED;EcxvEF;ICwEG,kBAAA;GfmrED;Ec3vEF;ICwEG,0BAAA;GfsrED;Ec9vEF;ICwEG,0BAAA;GfyrED;EcjwEF;ICwEG,iBAAA;Gf4rED;EcpwEF;ICwEG,0BAAA;Gf+rED;EcvwEF;ICwEG,0BAAA;GfksED;Ec1wEF;ICwEG,iBAAA;GfqsED;Ec7wEF;ICwEG,0BAAA;GfwsED;EchxEF;ICwEG,0BAAA;Gf2sED;EcnxEF;ICwEG,iBAAA;Gf8sED;EctxEF;ICwEG,0BAAA;GfitED;EczxEF;ICwEG,yBAAA;GfotED;Ec5xEF;ICwEG,gBAAA;GfutED;CACF;AgBzxED;EACE,8BAAA;ChB2xED;AgB5xED;EAQI,iBAAA;EACA,sBAAA;EACA,YAAA;ChBuxEH;AgBlxEG;;EACE,iBAAA;EACA,oBAAA;EACA,YAAA;ChBqxEL;AgBhxED;EACE,iBAAA;EACA,oBAAA;EACA,eAAA;EACA,iBAAA;ChBkxED;AgB/wED;EACE,iBAAA;ChBixED;AgB3wED;EACE,YAAA;EACA,gBAAA;EACA,oBAAA;ChB6wED;AgBhxED;;;;;;EAWQ,aAAA;EACA,wBAAA;EACA,oBAAA;EACA,2BAAA;ChB6wEP;AgB3xED;EAoBI,uBAAA;EACA,8BAAA;ChB0wEH;AgB/xED;;;;;;EA8BQ,cAAA;ChBywEP;AgBvyED;EAoCI,2BAAA;ChBswEH;AgB1yED;EAyCI,uBAAA;ChBowEH;AgB7vED;;;;;;EAOQ,aAAA;ChB8vEP;AgBnvED;EACE,uBAAA;ChBqvED;AgBtvED;;;;;;EAQQ,uBAAA;ChBsvEP;AgB9vED;;EAeM,yBAAA;ChBmvEL;AgBzuED;EAEI,0BAAA;ChB0uEH;AgBjuED;EAEI,0BAAA;ChBkuEH;AiBj3EC;;;;;;;;;;;;EAOI,0BAAA;CjBw3EL;AiBl3EC;;;;;EAMI,0BAAA;CjBm3EL;AiBt4EC;;;;;;;;;;;;EAOI,0BAAA;CjB64EL;AiBv4EC;;;;;EAMI,0BAAA;CjBw4EL;AiB35EC;;;;;;;;;;;;EAOI,0BAAA;CjBk6EL;AiB55EC;;;;;EAMI,0BAAA;CjB65EL;AiBh7EC;;;;;;;;;;;;EAOI,0BAAA;CjBu7EL;AiBj7EC;;;;;EAMI,0BAAA;CjBk7EL;AiBr8EC;;;;;;;;;;;;EAOI,0BAAA;CjB48EL;AiBt8EC;;;;;EAMI,0BAAA;CjBu8EL;AgBnzED;EACE,kBAAA;EACA,iBAAA;ChBqzED;AgBnzEC;EAAA;IACE,YAAA;IACA,oBAAA;IACA,mBAAA;IACA,6CAAA;IACA,uBAAA;GhBszED;EgB3zED;IASI,iBAAA;GhBqzEH;EgB9zED;;;;;;IAkBU,oBAAA;GhBozET;EgBt0ED;IA0BI,UAAA;GhB+yEH;EgBz0ED;;;;;;IAmCU,eAAA;GhB8yET;EgBj1ED;;;;;;IAuCU,gBAAA;GhBkzET;EgBz1ED;;;;IAoDU,iBAAA;GhB2yET;CACF;AkBrgFD;EAIE,aAAA;EACA,WAAA;EACA,UAAA;EACA,UAAA;ClBogFD;AkBjgFD;EACE,eAAA;EACA,YAAA;EACA,WAAA;EACA,oBAAA;EACA,gBAAA;EACA,qBAAA;EACA,eAAA;EACA,UAAA;EACA,iCAAA;ClBmgFD;AkBhgFD;EACE,sBAAA;EACA,gBAAA;EACA,mBAAA;EACA,iBAAA;ClBkgFD;AkBx/ED;Eb6BE,+BAAA;EACG,4BAAA;EACK,uBAAA;EarBR,yBAAA;EACA,sBAAA;EAAA,iBAAA;ClBo/ED;AkBh/ED;;EAEE,gBAAA;EACA,mBAAA;EACA,oBAAA;ClBk/ED;AkB5+EC;;;;;;EAGE,oBAAA;ClBi/EH;AkB7+ED;EACE,eAAA;ClB++ED;AkB3+ED;EACE,eAAA;EACA,YAAA;ClB6+ED;AkBz+ED;;EAEE,aAAA;ClB2+ED;AkBv+ED;;;EZ1FE,2CAAA;EACA,qBAAA;CNskFD;AkBt+ED;EACE,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;ClBw+ED;AkB98ED;EACE,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,uBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;Eb3EA,yDAAA;EACQ,iDAAA;EAyHR,+EAAA;EACK,0EAAA;EACG,uFAAA;EAAA,+EAAA;EAAA,uEAAA;EAAA,4GAAA;CLo6ET;AmB9iFC;EACE,sBAAA;EACA,WAAA;EdYF,0FAAA;EACQ,kFAAA;CLqiFT;AKpgFC;EACE,YAAA;EACA,WAAA;CLsgFH;AKpgFC;EAA0B,YAAA;CLugF3B;AKtgFC;EAAgC,YAAA;CLygFjC;AkB19EC;EACE,8BAAA;EACA,UAAA;ClB49EH;AkBp9EC;;;EAGE,0BAAA;EACA,WAAA;ClBs9EH;AkBn9EC;;EAEE,oBAAA;ClBq9EH;AkBj9EC;EACE,aAAA;ClBm9EH;AkBr8ED;EAKI;;;;IACE,kBAAA;GlBs8EH;EkBn8EC;;;;;;;;IAEE,kBAAA;GlB28EH;EkBx8EC;;;;;;;;IAEE,kBAAA;GlBg9EH;CACF;AkBt8ED;EACE,oBAAA;ClBw8ED;AkBh8ED;;EAEE,mBAAA;EACA,eAAA;EACA,iBAAA;EACA,oBAAA;ClBk8ED;AkB/7EC;;;;EAGI,oBAAA;ClBk8EL;AkB78ED;;EAgBI,iBAAA;EACA,mBAAA;EACA,iBAAA;EACA,iBAAA;EACA,gBAAA;ClBi8EH;AkB97ED;;;;EAIE,mBAAA;EACA,mBAAA;EACA,mBAAA;ClBg8ED;AkB77ED;;EAEE,iBAAA;ClB+7ED;AkB37ED;;EAEE,mBAAA;EACA,sBAAA;EACA,mBAAA;EACA,iBAAA;EACA,iBAAA;EACA,uBAAA;EACA,gBAAA;ClB67ED;AkB17EC;;;;EAEE,oBAAA;ClB87EH;AkB37ED;;EAEE,cAAA;EACA,kBAAA;ClB67ED;AkBp7ED;EACE,iBAAA;EAEA,iBAAA;EACA,oBAAA;EAEA,iBAAA;ClBo7ED;AkBl7EC;;EAEE,iBAAA;EACA,gBAAA;ClBo7EH;AkBv6ED;EC3PE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnBqqFD;AmBnqFC;EACE,aAAA;EACA,kBAAA;CnBqqFH;AmBlqFC;;EAEE,aAAA;CnBoqFH;AkBn7ED;EAEI,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;ClBo7EH;AkB17ED;EASI,aAAA;EACA,kBAAA;ClBo7EH;AkB97ED;;EAcI,aAAA;ClBo7EH;AkBl8ED;EAiBI,aAAA;EACA,iBAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;ClBo7EH;AkBh7ED;ECvRE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnB0sFD;AmBxsFC;EACE,aAAA;EACA,kBAAA;CnB0sFH;AmBvsFC;;EAEE,aAAA;CnBysFH;AkB57ED;EAEI,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;ClB67EH;AkBn8ED;EASI,aAAA;EACA,kBAAA;ClB67EH;AkBv8ED;;EAcI,aAAA;ClB67EH;AkB38ED;EAiBI,aAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;ClB67EH;AkBp7ED;EAEE,mBAAA;ClBq7ED;AkBv7ED;EAMI,sBAAA;ClBo7EH;AkBh7ED;EACE,mBAAA;EACA,OAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,mBAAA;EACA,qBAAA;ClBk7ED;AkBh7ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClBk7ED;AkBh7ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClBk7ED;AkB96ED;;;;;;;;;;EClZI,eAAA;CnB40FH;AkB17ED;EC9YI,sBAAA;EdiDF,yDAAA;EACQ,iDAAA;CL2xFT;AmB30FG;EACE,sBAAA;Ed8CJ,0EAAA;EACQ,kEAAA;CLgyFT;AkBp8ED;ECpYI,eAAA;EACA,0BAAA;EACA,sBAAA;CnB20FH;AkBz8ED;EC9XI,eAAA;CnB00FH;AkBz8ED;;;;;;;;;;ECrZI,eAAA;CnB02FH;AkBr9ED;ECjZI,sBAAA;EdiDF,yDAAA;EACQ,iDAAA;CLyzFT;AmBz2FG;EACE,sBAAA;Ed8CJ,0EAAA;EACQ,kEAAA;CL8zFT;AkB/9ED;ECvYI,eAAA;EACA,0BAAA;EACA,sBAAA;CnBy2FH;AkBp+ED;ECjYI,eAAA;CnBw2FH;AkBp+ED;;;;;;;;;;ECxZI,eAAA;CnBw4FH;AkBh/ED;ECpZI,sBAAA;EdiDF,yDAAA;EACQ,iDAAA;CLu1FT;AmBv4FG;EACE,sBAAA;Ed8CJ,0EAAA;EACQ,kEAAA;CL41FT;AkB1/ED;EC1YI,eAAA;EACA,0BAAA;EACA,sBAAA;CnBu4FH;AkB//ED;ECpYI,eAAA;CnBs4FH;AkB3/EC;EACE,UAAA;ClB6/EH;AkB3/EC;EACE,OAAA;ClB6/EH;AkBn/ED;EACE,eAAA;EACA,gBAAA;EACA,oBAAA;EACA,eAAA;ClBq/ED;AkBn+EC;EAAA;IAGI,sBAAA;IACA,iBAAA;IACA,uBAAA;GlBo+EH;EkBz+ED;IAUI,sBAAA;IACA,YAAA;IACA,uBAAA;GlBk+EH;EkB9+ED;IAiBI,sBAAA;GlBg+EH;EkBj/ED;IAqBI,sBAAA;IACA,uBAAA;GlB+9EH;EkBr/ED;;;IA2BM,YAAA;GlB+9EL;EkB1/ED;IAiCI,YAAA;GlB49EH;EkB7/ED;IAqCI,iBAAA;IACA,uBAAA;GlB29EH;EkBjgFD;;IA6CI,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlBw9EH;EkBxgFD;;IAmDM,gBAAA;GlBy9EL;EkB5gFD;;IAwDI,mBAAA;IACA,eAAA;GlBw9EH;EkBjhFD;IA8DI,OAAA;GlBs9EH;CACF;AkB58ED;;;;EASI,iBAAA;EACA,cAAA;EACA,iBAAA;ClBy8EH;AkBp9ED;;EAiBI,iBAAA;ClBu8EH;AkBx9ED;EJ9gBE,oBAAA;EACA,mBAAA;Cdy+FD;AkBj8EC;EAAA;IAEI,iBAAA;IACA,iBAAA;IACA,kBAAA;GlBm8EH;CACF;AkBn+ED;EAwCI,YAAA;ClB87EH;AkBt7EG;EAAA;IAEI,kBAAA;IACA,gBAAA;GlBw7EL;CACF;AkBp7EG;EAAA;IAEI,iBAAA;IACA,gBAAA;GlBs7EL;CACF;AoBrgGD;EACE,sBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,+BAAA;EAAA,2BAAA;EACA,gBAAA;EACA,uBAAA;EACA,8BAAA;ECoCA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,mBAAA;EhBqKA,0BAAA;EACG,uBAAA;EACC,sBAAA;EACI,kBAAA;CLg0FT;AoBxgGG;;;;;;EdrBF,2CAAA;EACA,qBAAA;CNqiGD;AoB3gGC;;;EAGE,YAAA;EACA,sBAAA;CpB6gGH;AoB1gGC;;EAEE,uBAAA;EACA,WAAA;Ef2BF,yDAAA;EACQ,iDAAA;CLk/FT;AoB1gGC;;;EAGE,oBAAA;EE9CF,0BAAA;EACA,cAAA;EjBiEA,yBAAA;EACQ,iBAAA;CL2/FT;AoB1gGG;;EAEE,qBAAA;CpB4gGL;AoBngGD;EC7DE,YAAA;EACA,uBAAA;EACA,mBAAA;CrBmkGD;AqBjkGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBmkGH;AqBjkGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBmkGH;AqBjkGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBmkGH;AqBjkGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBykGL;AqBnkGG;;;;;;;;;EAGE,uBAAA;EACA,mBAAA;CrB2kGL;AoBpjGD;EClBI,YAAA;EACA,uBAAA;CrBykGH;AoBrjGD;EChEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwnGD;AqBtnGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwnGH;AqBtnGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwnGH;AqBtnGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBwnGH;AqBtnGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB8nGL;AqBxnGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrBgoGL;AoBtmGD;ECrBI,eAAA;EACA,uBAAA;CrB8nGH;AoBtmGD;ECpEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6qGD;AqB3qGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6qGH;AqB3qGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6qGH;AqB3qGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrB6qGH;AqB3qGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBmrGL;AqB7qGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrBqrGL;AoBvpGD;ECzBI,eAAA;EACA,uBAAA;CrBmrGH;AoBvpGD;ECxEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBkuGD;AqBhuGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBkuGH;AqBhuGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBkuGH;AqBhuGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBkuGH;AqBhuGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwuGL;AqBluGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrB0uGL;AoBxsGD;EC7BI,eAAA;EACA,uBAAA;CrBwuGH;AoBxsGD;EC5EE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBuxGD;AqBrxGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBuxGH;AqBrxGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBuxGH;AqBrxGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBuxGH;AqBrxGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6xGL;AqBvxGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrB+xGL;AoBzvGD;ECjCI,eAAA;EACA,uBAAA;CrB6xGH;AoBzvGD;EChFE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB40GD;AqB10GC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB40GH;AqB10GC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB40GH;AqB10GC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrB40GH;AqB10GG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBk1GL;AqB50GG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrBo1GL;AoB1yGD;ECrCI,eAAA;EACA,uBAAA;CrBk1GH;AoBryGD;EACE,iBAAA;EACA,eAAA;EACA,iBAAA;CpBuyGD;AoBryGC;;;;;EAKE,8BAAA;EfnCF,yBAAA;EACQ,iBAAA;CL20GT;AoBtyGC;;;;EAIE,0BAAA;CpBwyGH;AoBtyGC;;EAEE,eAAA;EACA,2BAAA;EACA,8BAAA;CpBwyGH;AoBpyGG;;;;EAEE,eAAA;EACA,sBAAA;CpBwyGL;AoB/xGD;;EC9EE,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CrBi3GD;AoBlyGD;;EClFE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrBw3GD;AoBryGD;;ECtFE,iBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrB+3GD;AoBpyGD;EACE,eAAA;EACA,YAAA;CpBsyGD;AoBlyGD;EACE,gBAAA;CpBoyGD;AoB7xGC;;;EACE,YAAA;CpBiyGH;AuB37GD;EACE,WAAA;ElBoLA,yCAAA;EACK,oCAAA;EACG,iCAAA;CL0wGT;AuB77GC;EACE,WAAA;CvB+7GH;AuB37GD;EACE,cAAA;CvB67GD;AuB37GC;EAAY,eAAA;CvB87Gb;AuB77GC;EAAY,mBAAA;CvBg8Gb;AuB/7GC;EAAY,yBAAA;CvBk8Gb;AuB/7GD;EACE,mBAAA;EACA,UAAA;EACA,iBAAA;ElBsKA,gDAAA;EACQ,2CAAA;EAAA,wCAAA;EAOR,mCAAA;EACQ,8BAAA;EAAA,2BAAA;EAGR,yCAAA;EACQ,oCAAA;EAAA,iCAAA;CLoxGT;AwBh+GD;EACE,sBAAA;EACA,SAAA;EACA,UAAA;EACA,iBAAA;EACA,uBAAA;EACA,uBAAA;EACA,yBAAA;EACA,oCAAA;EACA,mCAAA;CxBk+GD;AwB99GD;;EAEE,mBAAA;CxBg+GD;AwB59GD;EACE,WAAA;CxB89GD;AwB19GD;EACE,mBAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,uBAAA;EACA,6BAAA;EACA,uBAAA;EACA,sCAAA;EACA,mBAAA;EnBuBA,oDAAA;EACQ,4CAAA;CLs8GT;AwBx9GC;EACE,SAAA;EACA,WAAA;CxB09GH;AwBn/GD;ECzBE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzB+gHD;AwBz/GD;EAmCI,eAAA;EACA,kBAAA;EACA,YAAA;EACA,iBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxBy9GH;AwBv9GG;;EAEE,eAAA;EACA,sBAAA;EACA,0BAAA;CxBy9GL;AwBl9GC;;;EAGE,YAAA;EACA,sBAAA;EACA,0BAAA;EACA,WAAA;CxBo9GH;AwB38GC;;;EAGE,eAAA;CxB68GH;AwBz8GC;;EAEE,sBAAA;EACA,oBAAA;EACA,8BAAA;EACA,uBAAA;EEzGF,oEAAA;C1BqjHD;AwBt8GD;EAGI,eAAA;CxBs8GH;AwBz8GD;EAQI,WAAA;CxBo8GH;AwB57GD;EACE,SAAA;EACA,WAAA;CxB87GD;AwBt7GD;EACE,YAAA;EACA,QAAA;CxBw7GD;AwBp7GD;EACE,eAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxBs7GD;AwBl7GD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,aAAA;CxBo7GD;AwBh7GD;EACE,SAAA;EACA,WAAA;CxBk7GD;AwB16GD;;EAII,YAAA;EACA,cAAA;EACA,0BAAA;EACA,4BAAA;CxB06GH;AwBj7GD;;EAWI,UAAA;EACA,aAAA;EACA,mBAAA;CxB06GH;AwBj6GD;EACE;IApEA,SAAA;IACA,WAAA;GxBw+GC;EwBr6GD;IA1DA,YAAA;IACA,QAAA;GxBk+GC;CACF;A2B7mHD;;EAEE,mBAAA;EACA,sBAAA;EACA,uBAAA;C3B+mHD;A2BnnHD;;EAMI,mBAAA;EACA,YAAA;C3BinHH;A2B/mHG;;;;;;;;EAIE,WAAA;C3BqnHL;A2B/mHD;;;;EAKI,kBAAA;C3BgnHH;A2B3mHD;EACE,kBAAA;C3B6mHD;A2B9mHD;;;EAOI,YAAA;C3B4mHH;A2BnnHD;;;EAYI,iBAAA;C3B4mHH;A2BxmHD;EACE,iBAAA;C3B0mHD;A2BtmHD;EACE,eAAA;C3BwmHD;A2BvmHC;ECpDA,2BAAA;EACA,8BAAA;C5B8pHD;A2BtmHD;;ECjDE,0BAAA;EACA,6BAAA;C5B2pHD;A2BrmHD;EACE,YAAA;C3BumHD;A2BrmHD;EACE,iBAAA;C3BumHD;A2BrmHD;;ECrEE,2BAAA;EACA,8BAAA;C5B8qHD;A2BpmHD;ECnEE,0BAAA;EACA,6BAAA;C5B0qHD;A2BnmHD;;EAEE,WAAA;C3BqmHD;A2BplHD;EACE,mBAAA;EACA,kBAAA;C3BslHD;A2BplHD;EACE,oBAAA;EACA,mBAAA;C3BslHD;A2BjlHD;EtB/CE,yDAAA;EACQ,iDAAA;CLmoHT;A2BjlHC;EtBnDA,yBAAA;EACQ,iBAAA;CLuoHT;A2B9kHD;EACE,eAAA;C3BglHD;A2B7kHD;EACE,wBAAA;EACA,uBAAA;C3B+kHD;A2B5kHD;EACE,wBAAA;C3B8kHD;A2BvkHD;;;EAII,eAAA;EACA,YAAA;EACA,YAAA;EACA,gBAAA;C3BwkHH;A2B/kHD;EAcM,YAAA;C3BokHL;A2BllHD;;;;EAsBI,iBAAA;EACA,eAAA;C3BkkHH;A2B7jHC;EACE,iBAAA;C3B+jHH;A2B7jHC;EC7KA,4BAAA;EACA,6BAAA;EAOA,8BAAA;EACA,6BAAA;C5BuuHD;A2B/jHC;ECjLA,0BAAA;EACA,2BAAA;EAOA,gCAAA;EACA,+BAAA;C5B6uHD;A2BhkHD;EACE,iBAAA;C3BkkHD;A2BhkHD;;ECjLE,8BAAA;EACA,6BAAA;C5BqvHD;A2B/jHD;EC/LE,0BAAA;EACA,2BAAA;C5BiwHD;A2B3jHD;EACE,eAAA;EACA,YAAA;EACA,oBAAA;EACA,0BAAA;C3B6jHD;A2BjkHD;;EAOI,oBAAA;EACA,YAAA;EACA,UAAA;C3B8jHH;A2BvkHD;EAYI,YAAA;C3B8jHH;A2B1kHD;EAgBI,WAAA;C3B6jHH;A2B5iHD;;;;EAKM,mBAAA;EACA,uBAAA;EACA,qBAAA;C3B6iHL;A6BvxHD;EACE,mBAAA;EACA,eAAA;EACA,0BAAA;C7ByxHD;A6BtxHC;EACE,YAAA;EACA,iBAAA;EACA,gBAAA;C7BwxHH;A6BjyHD;EAeI,mBAAA;EACA,WAAA;EAKA,YAAA;EAEA,YAAA;EACA,iBAAA;C7BgxHH;A6B9wHG;EACE,WAAA;C7BgxHL;A6BtwHD;;;EVwBE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnBmvHD;AmBjvHC;;;EACE,aAAA;EACA,kBAAA;CnBqvHH;AmBlvHC;;;;;;EAEE,aAAA;CnBwvHH;A6BxxHD;;;EVmBE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnB0wHD;AmBxwHC;;;EACE,aAAA;EACA,kBAAA;CnB4wHH;AmBzwHC;;;;;;EAEE,aAAA;CnB+wHH;A6BtyHD;;;EAGE,oBAAA;C7BwyHD;A6BtyHC;;;EACE,iBAAA;C7B0yHH;A6BtyHD;;EAEE,UAAA;EACA,oBAAA;EACA,uBAAA;C7BwyHD;A6BnyHD;EACE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,0BAAA;EACA,uBAAA;EACA,mBAAA;C7BqyHD;A6BlyHC;EACE,kBAAA;EACA,gBAAA;EACA,mBAAA;C7BoyHH;A6BlyHC;EACE,mBAAA;EACA,gBAAA;EACA,mBAAA;C7BoyHH;A6BxzHD;;EA0BI,cAAA;C7BkyHH;A6B7xHD;;;;;;;EDtGE,2BAAA;EACA,8BAAA;C5B44HD;A6B9xHD;EACE,gBAAA;C7BgyHD;A6B9xHD;;;;;;;ED1GE,0BAAA;EACA,6BAAA;C5Bi5HD;A6B/xHD;EACE,eAAA;C7BiyHD;A6B5xHD;EACE,mBAAA;EAGA,aAAA;EACA,oBAAA;C7B4xHD;A6BjyHD;EAUI,mBAAA;C7B0xHH;A6BpyHD;EAYM,kBAAA;C7B2xHL;A6BxxHG;;;EAGE,WAAA;C7B0xHL;A6BrxHC;;EAGI,mBAAA;C7BsxHL;A6BnxHC;;EAGI,WAAA;EACA,kBAAA;C7BoxHL;A8Bn7HD;EACE,gBAAA;EACA,iBAAA;EACA,iBAAA;C9Bq7HD;A8Bx7HD;EAOI,mBAAA;EACA,eAAA;C9Bo7HH;A8B57HD;EAWM,mBAAA;EACA,eAAA;EACA,mBAAA;C9Bo7HL;A8Bn7HK;;EAEE,sBAAA;EACA,0BAAA;C9Bq7HP;A8Bh7HG;EACE,eAAA;C9Bk7HL;A8Bh7HK;;EAEE,eAAA;EACA,sBAAA;EACA,oBAAA;EACA,8BAAA;C9Bk7HP;A8B36HG;;;EAGE,0BAAA;EACA,sBAAA;C9B66HL;A8Bt9HD;ELLE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzB89HD;A8B59HD;EA0DI,gBAAA;C9Bq6HH;A8B55HD;EACE,8BAAA;C9B85HD;A8B/5HD;EAGI,YAAA;EAEA,oBAAA;C9B85HH;A8Bn6HD;EASM,kBAAA;EACA,wBAAA;EACA,8BAAA;EACA,2BAAA;C9B65HL;A8B55HK;EACE,mCAAA;C9B85HP;A8Bx5HK;;;EAGE,eAAA;EACA,gBAAA;EACA,uBAAA;EACA,uBAAA;EACA,iCAAA;C9B05HP;A8Br5HC;EAqDA,YAAA;EA8BA,iBAAA;C9Bs0HD;A8Bz5HC;EAwDE,YAAA;C9Bo2HH;A8B55HC;EA0DI,mBAAA;EACA,mBAAA;C9Bq2HL;A8Bh6HC;EAgEE,UAAA;EACA,WAAA;C9Bm2HH;A8Bh2HC;EAAA;IAEI,oBAAA;IACA,UAAA;G9Bk2HH;E8Br2HD;IAKM,iBAAA;G9Bm2HL;CACF;A8B76HC;EAuFE,gBAAA;EACA,mBAAA;C9By1HH;A8Bj7HC;;;EA8FE,uBAAA;C9Bw1HH;A8Br1HC;EAAA;IAEI,8BAAA;IACA,2BAAA;G9Bu1HH;E8B11HD;;;IAQI,0BAAA;G9Bu1HH;CACF;A8Bx7HD;EAEI,YAAA;C9By7HH;A8B37HD;EAMM,mBAAA;C9Bw7HL;A8B97HD;EASM,iBAAA;C9Bw7HL;A8Bn7HK;;;EAGE,YAAA;EACA,0BAAA;C9Bq7HP;A8B76HD;EAEI,YAAA;C9B86HH;A8Bh7HD;EAIM,gBAAA;EACA,eAAA;C9B+6HL;A8Bn6HD;EACE,YAAA;C9Bq6HD;A8Bt6HD;EAII,YAAA;C9Bq6HH;A8Bz6HD;EAMM,mBAAA;EACA,mBAAA;C9Bs6HL;A8B76HD;EAYI,UAAA;EACA,WAAA;C9Bo6HH;A8Bj6HC;EAAA;IAEI,oBAAA;IACA,UAAA;G9Bm6HH;E8Bt6HD;IAKM,iBAAA;G9Bo6HL;CACF;A8B55HD;EACE,iBAAA;C9B85HD;A8B/5HD;EAKI,gBAAA;EACA,mBAAA;C9B65HH;A8Bn6HD;;;EAYI,uBAAA;C9B45HH;A8Bz5HC;EAAA;IAEI,8BAAA;IACA,2BAAA;G9B25HH;E8B95HD;;;IAQI,0BAAA;G9B25HH;CACF;A8Bl5HD;EAEI,cAAA;C9Bm5HH;A8Br5HD;EAKI,eAAA;C9Bm5HH;A8B14HD;EAEE,iBAAA;EF7OA,0BAAA;EACA,2BAAA;C5BynID;A+BjnID;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;EACA,8BAAA;C/BmnID;A+B9mIC;EAAA;IACE,mBAAA;G/BinID;CACF;A+BrmIC;EAAA;IACE,YAAA;G/BwmID;CACF;A+B1lID;EACE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,kCAAA;EACA,2DAAA;EAAA,mDAAA;EAEA,kCAAA;C/B2lID;A+BzlIC;EACE,iBAAA;C/B2lIH;A+BxlIC;EAAA;IACE,YAAA;IACA,cAAA;IACA,yBAAA;IAAA,iBAAA;G/B2lID;E+BzlIC;IACE,0BAAA;IACA,wBAAA;IACA,kBAAA;IACA,6BAAA;G/B2lIH;E+BxlIC;IACE,oBAAA;G/B0lIH;E+BrlIC;;;IAGE,iBAAA;IACA,gBAAA;G/BulIH;CACF;A+BnlID;;EAWE,gBAAA;EACA,SAAA;EACA,QAAA;EACA,cAAA;C/B4kID;A+B1lID;;EAGI,kBAAA;C/B2lIH;A+BzlIG;EAAA;;IACE,kBAAA;G/B6lIH;CACF;A+BnlIC;EAAA;;IACE,iBAAA;G/BulID;CACF;A+BplID;EACE,OAAA;EACA,sBAAA;C/BslID;A+BplID;EACE,UAAA;EACA,iBAAA;EACA,sBAAA;C/BslID;A+B9kID;;;;EAII,oBAAA;EACA,mBAAA;C/BglIH;A+B9kIG;EAAA;;;;IACE,gBAAA;IACA,eAAA;G/BolIH;CACF;A+BxkID;EACE,cAAA;EACA,sBAAA;C/B0kID;A+BxkIC;EAAA;IACE,iBAAA;G/B2kID;CACF;A+BrkID;EACE,YAAA;EACA,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,kBAAA;C/BukID;A+BrkIC;;EAEE,sBAAA;C/BukIH;A+BhlID;EAaI,eAAA;C/BskIH;A+BnkIC;EACE;;IAEE,mBAAA;G/BqkIH;CACF;A+B3jID;EACE,mBAAA;EACA,aAAA;EACA,kBAAA;EACA,mBAAA;EC9LA,gBAAA;EACA,mBAAA;ED+LA,8BAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;C/B8jID;A+B1jIC;EACE,WAAA;C/B4jIH;A+B1kID;EAmBI,eAAA;EACA,YAAA;EACA,YAAA;EACA,mBAAA;C/B0jIH;A+BhlID;EAyBI,gBAAA;C/B0jIH;A+BvjIC;EAAA;IACE,cAAA;G/B0jID;CACF;A+BjjID;EACE,oBAAA;C/BmjID;A+BpjID;EAII,kBAAA;EACA,qBAAA;EACA,kBAAA;C/BmjIH;A+BhjIC;EAAA;IAGI,iBAAA;IACA,YAAA;IACA,YAAA;IACA,cAAA;IACA,8BAAA;IACA,UAAA;IACA,yBAAA;IAAA,iBAAA;G/BijIH;E+B1jID;;IAYM,2BAAA;G/BkjIL;E+B9jID;IAeM,kBAAA;G/BkjIL;E+BjjIK;;IAEE,uBAAA;G/BmjIP;CACF;A+B7iIC;EAAA;IACE,YAAA;IACA,UAAA;G/BgjID;E+BljID;IAKI,YAAA;G/BgjIH;E+BrjID;IAOM,kBAAA;IACA,qBAAA;G/BijIL;CACF;A+BtiID;EACE,mBAAA;EACA,oBAAA;EACA,mBAAA;EACA,kCAAA;EACA,qCAAA;E1B5NA,6FAAA;EACQ,qFAAA;E2BjER,gBAAA;EACA,mBAAA;ChCu0ID;AkB13HC;EAAA;IAGI,sBAAA;IACA,iBAAA;IACA,uBAAA;GlB23HH;EkBh4HD;IAUI,sBAAA;IACA,YAAA;IACA,uBAAA;GlBy3HH;EkBr4HD;IAiBI,sBAAA;GlBu3HH;EkBx4HD;IAqBI,sBAAA;IACA,uBAAA;GlBs3HH;EkB54HD;;;IA2BM,YAAA;GlBs3HL;EkBj5HD;IAiCI,YAAA;GlBm3HH;EkBp5HD;IAqCI,iBAAA;IACA,uBAAA;GlBk3HH;EkBx5HD;;IA6CI,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlB+2HH;EkB/5HD;;IAmDM,gBAAA;GlBg3HL;EkBn6HD;;IAwDI,mBAAA;IACA,eAAA;GlB+2HH;EkBx6HD;IA8DI,OAAA;GlB62HH;CACF;A+BtlIG;EAAA;IACE,mBAAA;G/BylIH;E+BvlIG;IACE,iBAAA;G/BylIL;CACF;A+BjlIC;EAAA;IACE,YAAA;IACA,eAAA;IACA,kBAAA;IACA,gBAAA;IACA,eAAA;IACA,UAAA;I1BvPF,yBAAA;IACQ,iBAAA;GL40IP;CACF;A+B9kID;EACE,cAAA;EHpUA,0BAAA;EACA,2BAAA;C5Bq5ID;A+B9kID;EACE,iBAAA;EHzUA,4BAAA;EACA,6BAAA;EAOA,8BAAA;EACA,6BAAA;C5Bo5ID;A+B1kID;EChVE,gBAAA;EACA,mBAAA;ChC65ID;A+B3kIC;ECnVA,iBAAA;EACA,oBAAA;ChCi6ID;A+B5kIC;ECtVA,iBAAA;EACA,oBAAA;ChCq6ID;A+BtkID;EChWE,iBAAA;EACA,oBAAA;ChCy6ID;A+BvkIC;EAAA;IACE,YAAA;IACA,mBAAA;IACA,kBAAA;G/B0kID;CACF;A+B9jID;EACE;IEtWA,uBAAA;GjCu6IC;E+BhkID;IE1WA,wBAAA;IF4WE,oBAAA;G/BkkID;E+BpkID;IAKI,gBAAA;G/BkkIH;CACF;A+BzjID;EACE,0BAAA;EACA,sBAAA;C/B2jID;A+B7jID;EAKI,YAAA;C/B2jIH;A+B1jIG;;EAEE,eAAA;EACA,8BAAA;C/B4jIL;A+BrkID;EAcI,YAAA;C/B0jIH;A+BxkID;EAmBM,YAAA;C/BwjIL;A+BtjIK;;EAEE,YAAA;EACA,8BAAA;C/BwjIP;A+BpjIK;;;EAGE,YAAA;EACA,0BAAA;C/BsjIP;A+BljIK;;;EAGE,YAAA;EACA,8BAAA;C/BojIP;A+B7iIK;;;EAGE,YAAA;EACA,0BAAA;C/B+iIP;A+B3iIG;EAAA;IAIM,YAAA;G/B2iIP;E+B1iIO;;IAEE,YAAA;IACA,8BAAA;G/B4iIT;E+BxiIO;;;IAGE,YAAA;IACA,0BAAA;G/B0iIT;E+BtiIO;;;IAGE,YAAA;IACA,8BAAA;G/BwiIT;CACF;A+BxnID;EAuFI,mBAAA;C/BoiIH;A+BniIG;;EAEE,uBAAA;C/BqiIL;A+B/nID;EA6FM,uBAAA;C/BqiIL;A+BloID;;EAmGI,sBAAA;C/BmiIH;A+BtoID;EA4GI,YAAA;C/B6hIH;A+B5hIG;EACE,YAAA;C/B8hIL;A+B5oID;EAmHI,YAAA;C/B4hIH;A+B3hIG;;EAEE,YAAA;C/B6hIL;A+BzhIK;;;;EAEE,YAAA;C/B6hIP;A+BrhID;EACE,uBAAA;EACA,sBAAA;C/BuhID;A+BzhID;EAKI,eAAA;C/BuhIH;A+BthIG;;EAEE,YAAA;EACA,8BAAA;C/BwhIL;A+BjiID;EAcI,eAAA;C/BshIH;A+BpiID;EAmBM,eAAA;C/BohIL;A+BlhIK;;EAEE,YAAA;EACA,8BAAA;C/BohIP;A+BhhIK;;;EAGE,YAAA;EACA,0BAAA;C/BkhIP;A+B9gIK;;;EAGE,YAAA;EACA,8BAAA;C/BghIP;A+B1gIK;;;EAGE,YAAA;EACA,0BAAA;C/B4gIP;A+BxgIG;EAAA;IAIM,sBAAA;G/BwgIP;E+B5gIC;IAOM,0BAAA;G/BwgIP;E+B/gIC;IAUM,eAAA;G/BwgIP;E+BvgIO;;IAEE,YAAA;IACA,8BAAA;G/BygIT;E+BrgIO;;;IAGE,YAAA;IACA,0BAAA;G/BugIT;E+BngIO;;;IAGE,YAAA;IACA,8BAAA;G/BqgIT;CACF;A+B1lID;EA6FI,mBAAA;C/BggIH;A+B//HG;;EAEE,uBAAA;C/BigIL;A+BjmID;EAmGM,uBAAA;C/BigIL;A+BpmID;;EAyGI,sBAAA;C/B+/HH;A+BxmID;EA6GI,eAAA;C/B8/HH;A+B7/HG;EACE,YAAA;C/B+/HL;A+B9mID;EAoHI,eAAA;C/B6/HH;A+B5/HG;;EAEE,YAAA;C/B8/HL;A+B1/HK;;;;EAEE,YAAA;C/B8/HP;AkCpoJD;EACE,kBAAA;EACA,oBAAA;EACA,iBAAA;EACA,0BAAA;EACA,mBAAA;ClCsoJD;AkC3oJD;EAQI,sBAAA;ClCsoJH;AkC9oJD;EAWM,eAAA;EACA,YAAA;EACA,kBAAA;ClCsoJL;AkCnpJD;EAkBI,eAAA;ClCooJH;AmCxpJD;EACE,sBAAA;EACA,gBAAA;EACA,eAAA;EACA,mBAAA;CnC0pJD;AmC9pJD;EAOI,gBAAA;CnC0pJH;AmCjqJD;;EAUM,mBAAA;EACA,YAAA;EACA,kBAAA;EACA,kBAAA;EACA,wBAAA;EACA,eAAA;EACA,sBAAA;EACA,uBAAA;EACA,uBAAA;CnC2pJL;AmCzpJK;;;;EAEE,WAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CnC6pJP;AmC1pJG;;EAGI,eAAA;EPnBN,4BAAA;EACA,+BAAA;C5B+qJD;AmCzpJG;;EP/BF,6BAAA;EACA,gCAAA;C5B4rJD;AmCppJG;;;;;;EAGE,WAAA;EACA,YAAA;EACA,gBAAA;EACA,0BAAA;EACA,sBAAA;CnCypJL;AmC7sJD;;;;;;EA+DM,eAAA;EACA,oBAAA;EACA,uBAAA;EACA,mBAAA;CnCspJL;AmC7oJD;;ECxEM,mBAAA;EACA,gBAAA;EACA,uBAAA;CpCytJL;AoCvtJG;;ERKF,4BAAA;EACA,+BAAA;C5BstJD;AoCttJG;;ERTF,6BAAA;EACA,gCAAA;C5BmuJD;AmCxpJD;;EC7EM,kBAAA;EACA,gBAAA;EACA,iBAAA;CpCyuJL;AoCvuJG;;ERKF,4BAAA;EACA,+BAAA;C5BsuJD;AoCtuJG;;ERTF,6BAAA;EACA,gCAAA;C5BmvJD;AqCtvJD;EACE,gBAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;CrCwvJD;AqC5vJD;EAOI,gBAAA;CrCwvJH;AqC/vJD;;EAUM,sBAAA;EACA,kBAAA;EACA,uBAAA;EACA,uBAAA;EACA,oBAAA;CrCyvJL;AqCvwJD;;EAmBM,sBAAA;EACA,0BAAA;CrCwvJL;AqC5wJD;;EA2BM,aAAA;CrCqvJL;AqChxJD;;EAkCM,YAAA;CrCkvJL;AqCpxJD;;;;EA2CM,eAAA;EACA,oBAAA;EACA,uBAAA;CrC+uJL;AsC7xJD;EACE,gBAAA;EACA,2BAAA;EACA,eAAA;EACA,iBAAA;EACA,eAAA;EACA,YAAA;EACA,mBAAA;EACA,oBAAA;EACA,yBAAA;EACA,sBAAA;CtC+xJD;AsC3xJG;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;CtC6xJL;AsCxxJC;EACE,cAAA;CtC0xJH;AsCtxJC;EACE,mBAAA;EACA,UAAA;CtCwxJH;AsCjxJD;ECtCE,0BAAA;CvC0zJD;AuCvzJG;;EAEE,0BAAA;CvCyzJL;AsCpxJD;EC1CE,0BAAA;CvCi0JD;AuC9zJG;;EAEE,0BAAA;CvCg0JL;AsCvxJD;EC9CE,0BAAA;CvCw0JD;AuCr0JG;;EAEE,0BAAA;CvCu0JL;AsC1xJD;EClDE,0BAAA;CvC+0JD;AuC50JG;;EAEE,0BAAA;CvC80JL;AsC7xJD;ECtDE,0BAAA;CvCs1JD;AuCn1JG;;EAEE,0BAAA;CvCq1JL;AsChyJD;EC1DE,0BAAA;CvC61JD;AuC11JG;;EAEE,0BAAA;CvC41JL;AwC91JD;EACE,sBAAA;EACA,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,YAAA;EACA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,0BAAA;EACA,oBAAA;CxCg2JD;AwC71JC;EACE,cAAA;CxC+1JH;AwC31JC;EACE,mBAAA;EACA,UAAA;CxC61JH;AwC11JC;;EAEE,OAAA;EACA,iBAAA;CxC41JH;AwCv1JG;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;CxCy1JL;AwCp1JC;;EAEE,eAAA;EACA,uBAAA;CxCs1JH;AwCn1JC;EACE,aAAA;CxCq1JH;AwCl1JC;EACE,kBAAA;CxCo1JH;AwCj1JC;EACE,iBAAA;CxCm1JH;AyC74JD;EACE,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,eAAA;EACA,0BAAA;CzC+4JD;AyCp5JD;;EASI,eAAA;CzC+4JH;AyCx5JD;EAaI,oBAAA;EACA,gBAAA;EACA,iBAAA;CzC84JH;AyC75JD;EAmBI,0BAAA;CzC64JH;AyC14JC;;EAEE,oBAAA;EACA,mBAAA;EACA,mBAAA;CzC44JH;AyCt6JD;EA8BI,gBAAA;CzC24JH;AyCx4JC;EAAA;IACE,kBAAA;IACA,qBAAA;GzC24JD;EyCz4JC;;IAEE,oBAAA;IACA,mBAAA;GzC24JH;EyCl5JD;;IAYI,gBAAA;GzC04JH;CACF;A0Cr7JD;EACE,eAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;ErCiLA,4CAAA;EACK,uCAAA;EACG,oCAAA;CLuwJT;A0Cj8JD;;EAaI,mBAAA;EACA,kBAAA;C1Cw7JH;A0Cp7JC;;;EAGE,sBAAA;C1Cs7JH;A0C38JD;EA0BI,aAAA;EACA,eAAA;C1Co7JH;A2C/8JD;EACE,cAAA;EACA,oBAAA;EACA,8BAAA;EACA,mBAAA;C3Ci9JD;A2Cr9JD;EAQI,cAAA;EACA,eAAA;C3Cg9JH;A2Cz9JD;EAcI,kBAAA;C3C88JH;A2C59JD;;EAoBI,iBAAA;C3C48JH;A2Ch+JD;EAwBI,gBAAA;C3C28JH;A2Cl8JD;;EAEE,oBAAA;C3Co8JD;A2Ct8JD;;EAMI,mBAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;C3Co8JH;A2C57JD;ECvDE,eAAA;EACA,0BAAA;EACA,sBAAA;C5Cs/JD;A2Cj8JD;EClDI,0BAAA;C5Cs/JH;A2Cp8JD;EC9CI,eAAA;C5Cq/JH;A2Cn8JD;EC3DE,eAAA;EACA,0BAAA;EACA,sBAAA;C5CigKD;A2Cx8JD;ECtDI,0BAAA;C5CigKH;A2C38JD;EClDI,eAAA;C5CggKH;A2C18JD;EC/DE,eAAA;EACA,0BAAA;EACA,sBAAA;C5C4gKD;A2C/8JD;EC1DI,0BAAA;C5C4gKH;A2Cl9JD;ECtDI,eAAA;C5C2gKH;A2Cj9JD;ECnEE,eAAA;EACA,0BAAA;EACA,sBAAA;C5CuhKD;A2Ct9JD;EC9DI,0BAAA;C5CuhKH;A2Cz9JD;EC1DI,eAAA;C5CshKH;A6CvhKD;EACE;IAAQ,4BAAA;G7C0hKP;E6CzhKD;IAAQ,yBAAA;G7C4hKP;CACF;A6CzhKD;EACE;IAAQ,4BAAA;G7C4hKP;E6C3hKD;IAAQ,yBAAA;G7C8hKP;CACF;A6CjiKD;EACE;IAAQ,4BAAA;G7C4hKP;E6C3hKD;IAAQ,yBAAA;G7C8hKP;CACF;A6CvhKD;EACE,aAAA;EACA,oBAAA;EACA,iBAAA;EACA,0BAAA;EACA,mBAAA;ExCsCA,uDAAA;EACQ,+CAAA;CLo/JT;A6CthKD;EACE,YAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,YAAA;EACA,mBAAA;EACA,0BAAA;ExCyBA,uDAAA;EACQ,+CAAA;EAyHR,oCAAA;EACK,+BAAA;EACG,4BAAA;CLw4JT;A6CnhKD;;ECDI,8MAAA;EACA,yMAAA;EACA,sMAAA;EDEF,mCAAA;EAAA,2BAAA;C7CuhKD;A6ChhKD;;ExC5CE,2DAAA;EACK,sDAAA;EACG,mDAAA;CLgkKT;A6C7gKD;EEvEE,0BAAA;C/CulKD;A+CplKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9CuiKH;A6CjhKD;EE3EE,0BAAA;C/C+lKD;A+C5lKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C+iKH;A6CrhKD;EE/EE,0BAAA;C/CumKD;A+CpmKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9CujKH;A6CzhKD;EEnFE,0BAAA;C/C+mKD;A+C5mKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C+jKH;AgDvnKD;EAEE,iBAAA;ChDwnKD;AgDtnKC;EACE,cAAA;ChDwnKH;AgDpnKD;;EAEE,iBAAA;EACA,QAAA;ChDsnKD;AgDnnKD;EACE,eAAA;ChDqnKD;AgDlnKD;EACE,eAAA;ChDonKD;AgDjnKC;EACE,gBAAA;ChDmnKH;AgD/mKD;;EAEE,mBAAA;ChDinKD;AgD9mKD;;EAEE,oBAAA;ChDgnKD;AgD7mKD;;;EAGE,oBAAA;EACA,oBAAA;ChD+mKD;AgD5mKD;EACE,uBAAA;ChD8mKD;AgD3mKD;EACE,uBAAA;ChD6mKD;AgDzmKD;EACE,cAAA;EACA,mBAAA;ChD2mKD;AgDrmKD;EACE,gBAAA;EACA,iBAAA;ChDumKD;AiD5pKD;EAEE,gBAAA;EACA,oBAAA;CjD6pKD;AiDrpKD;EACE,mBAAA;EACA,eAAA;EACA,mBAAA;EAEA,oBAAA;EACA,uBAAA;EACA,uBAAA;CjDspKD;AiDnpKC;ErB7BA,4BAAA;EACA,6BAAA;C5BmrKD;AiDppKC;EACE,iBAAA;ErBzBF,gCAAA;EACA,+BAAA;C5BgrKD;AiDnpKC;;;EAGE,eAAA;EACA,oBAAA;EACA,0BAAA;CjDqpKH;AiD1pKC;;;EASI,eAAA;CjDspKL;AiD/pKC;;;EAYI,eAAA;CjDwpKL;AiDnpKC;;;EAGE,WAAA;EACA,YAAA;EACA,0BAAA;EACA,sBAAA;CjDqpKH;AiD3pKC;;;;;;;;;EAYI,eAAA;CjD0pKL;AiDtqKC;;;EAeI,eAAA;CjD4pKL;AiDjpKD;;EAEE,YAAA;CjDmpKD;AiDrpKD;;EAKI,YAAA;CjDopKH;AiDhpKC;;;;EAEE,YAAA;EACA,sBAAA;EACA,0BAAA;CjDopKH;AiDhpKD;EACE,YAAA;EACA,iBAAA;CjDkpKD;AczvKA;EoCIG,eAAA;EACA,0BAAA;ClDwvKH;AkDtvKG;;EAEE,eAAA;ClDwvKL;AkD1vKG;;EAKI,eAAA;ClDyvKP;AkDtvKK;;;;EAEE,eAAA;EACA,0BAAA;ClD0vKP;AkDxvKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD6vKP;ActxKA;EoCIG,eAAA;EACA,0BAAA;ClDqxKH;AkDnxKG;;EAEE,eAAA;ClDqxKL;AkDvxKG;;EAKI,eAAA;ClDsxKP;AkDnxKK;;;;EAEE,eAAA;EACA,0BAAA;ClDuxKP;AkDrxKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD0xKP;AcnzKA;EoCIG,eAAA;EACA,0BAAA;ClDkzKH;AkDhzKG;;EAEE,eAAA;ClDkzKL;AkDpzKG;;EAKI,eAAA;ClDmzKP;AkDhzKK;;;;EAEE,eAAA;EACA,0BAAA;ClDozKP;AkDlzKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDuzKP;Ach1KA;EoCIG,eAAA;EACA,0BAAA;ClD+0KH;AkD70KG;;EAEE,eAAA;ClD+0KL;AkDj1KG;;EAKI,eAAA;ClDg1KP;AkD70KK;;;;EAEE,eAAA;EACA,0BAAA;ClDi1KP;AkD/0KK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDo1KP;AiDnvKD;EACE,cAAA;EACA,mBAAA;CjDqvKD;AiDnvKD;EACE,iBAAA;EACA,iBAAA;CjDqvKD;AmD72KD;EACE,oBAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;E9C0DA,kDAAA;EACQ,0CAAA;CLszKT;AmD52KD;EACE,cAAA;CnD82KD;AmDz2KD;EACE,mBAAA;EACA,qCAAA;EvBtBA,4BAAA;EACA,6BAAA;C5Bk4KD;AmD/2KD;EAMI,eAAA;CnD42KH;AmDv2KD;EACE,cAAA;EACA,iBAAA;EACA,gBAAA;EACA,eAAA;CnDy2KD;AmD72KD;;;;;EAWI,eAAA;CnDy2KH;AmDp2KD;EACE,mBAAA;EACA,0BAAA;EACA,2BAAA;EvB1CA,gCAAA;EACA,+BAAA;C5Bi5KD;AmD91KD;;EAGI,iBAAA;CnD+1KH;AmDl2KD;;EAMM,oBAAA;EACA,iBAAA;CnDg2KL;AmD51KG;;EAEI,cAAA;EvBzEN,4BAAA;EACA,6BAAA;C5Bw6KD;AmD11KG;;EAEI,iBAAA;EvBzEN,gCAAA;EACA,+BAAA;C5Bs6KD;AmDn3KD;EvB5DE,0BAAA;EACA,2BAAA;C5Bk7KD;AmDt1KD;EAEI,oBAAA;CnDu1KH;AmDp1KD;EACE,oBAAA;CnDs1KD;AmD90KD;;;EAII,iBAAA;CnD+0KH;AmDn1KD;;;EAOM,oBAAA;EACA,mBAAA;CnDi1KL;AmDz1KD;;EvB3GE,4BAAA;EACA,6BAAA;C5Bw8KD;AmD91KD;;;;EAmBQ,4BAAA;EACA,6BAAA;CnDi1KP;AmDr2KD;;;;;;;;EAwBU,4BAAA;CnDu1KT;AmD/2KD;;;;;;;;EA4BU,6BAAA;CnD61KT;AmDz3KD;;EvBnGE,gCAAA;EACA,+BAAA;C5Bg+KD;AmD93KD;;;;EAyCQ,gCAAA;EACA,+BAAA;CnD21KP;AmDr4KD;;;;;;;;EA8CU,+BAAA;CnDi2KT;AmD/4KD;;;;;;;;EAkDU,gCAAA;CnDu2KT;AmDz5KD;;;;EA2DI,2BAAA;CnDo2KH;AmD/5KD;;EA+DI,cAAA;CnDo2KH;AmDn6KD;;EAmEI,UAAA;CnDo2KH;AmDv6KD;;;;;;;;;;;;EA0EU,eAAA;CnD22KT;AmDr7KD;;;;;;;;;;;;EA8EU,gBAAA;CnDq3KT;AmDn8KD;;;;;;;;EAuFU,iBAAA;CnDs3KT;AmD78KD;;;;;;;;EAgGU,iBAAA;CnDu3KT;AmDv9KD;EAsGI,iBAAA;EACA,UAAA;CnDo3KH;AmD12KD;EACE,oBAAA;CnD42KD;AmD72KD;EAKI,iBAAA;EACA,mBAAA;CnD22KH;AmDj3KD;EASM,gBAAA;CnD22KL;AmDp3KD;EAcI,iBAAA;CnDy2KH;AmDv3KD;;EAkBM,2BAAA;CnDy2KL;AmD33KD;EAuBI,cAAA;CnDu2KH;AmD93KD;EAyBM,8BAAA;CnDw2KL;AmDj2KD;EC5PE,mBAAA;CpDgmLD;AoD9lLC;EACE,eAAA;EACA,0BAAA;EACA,mBAAA;CpDgmLH;AoDnmLC;EAMI,uBAAA;CpDgmLL;AoDtmLC;EASI,eAAA;EACA,0BAAA;CpDgmLL;AoD7lLC;EAEI,0BAAA;CpD8lLL;AmDh3KD;EC/PE,sBAAA;CpDknLD;AoDhnLC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CpDknLH;AoDrnLC;EAMI,0BAAA;CpDknLL;AoDxnLC;EASI,eAAA;EACA,uBAAA;CpDknLL;AoD/mLC;EAEI,6BAAA;CpDgnLL;AmD/3KD;EClQE,sBAAA;CpDooLD;AoDloLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDooLH;AoDvoLC;EAMI,0BAAA;CpDooLL;AoD1oLC;EASI,eAAA;EACA,0BAAA;CpDooLL;AoDjoLC;EAEI,6BAAA;CpDkoLL;AmD94KD;ECrQE,sBAAA;CpDspLD;AoDppLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDspLH;AoDzpLC;EAMI,0BAAA;CpDspLL;AoD5pLC;EASI,eAAA;EACA,0BAAA;CpDspLL;AoDnpLC;EAEI,6BAAA;CpDopLL;AmD75KD;ECxQE,sBAAA;CpDwqLD;AoDtqLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDwqLH;AoD3qLC;EAMI,0BAAA;CpDwqLL;AoD9qLC;EASI,eAAA;EACA,0BAAA;CpDwqLL;AoDrqLC;EAEI,6BAAA;CpDsqLL;AmD56KD;EC3QE,sBAAA;CpD0rLD;AoDxrLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpD0rLH;AoD7rLC;EAMI,0BAAA;CpD0rLL;AoDhsLC;EASI,eAAA;EACA,0BAAA;CpD0rLL;AoDvrLC;EAEI,6BAAA;CpDwrLL;AqDxsLD;EACE,mBAAA;EACA,eAAA;EACA,UAAA;EACA,WAAA;EACA,iBAAA;CrD0sLD;AqD/sLD;;;;;EAYI,mBAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA;EACA,YAAA;EACA,aAAA;EACA,UAAA;CrD0sLH;AqDrsLD;EACE,uBAAA;CrDusLD;AqDnsLD;EACE,oBAAA;CrDqsLD;AsDhuLD;EACE,iBAAA;EACA,cAAA;EACA,oBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;EjD0DA,wDAAA;EACQ,gDAAA;CLyqLT;AsD1uLD;EASI,mBAAA;EACA,kCAAA;CtDouLH;AsD/tLD;EACE,cAAA;EACA,mBAAA;CtDiuLD;AsD/tLD;EACE,aAAA;EACA,mBAAA;CtDiuLD;AuDrvLD;EACE,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,YAAA;EACA,0BAAA;EjCTA,0BAAA;EACA,aAAA;CtBiwLD;AuDtvLC;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;EjChBF,0BAAA;EACA,aAAA;CtBywLD;AuDlvLC;EACE,WAAA;EACA,gBAAA;EACA,wBAAA;EACA,UAAA;EACA,yBAAA;EACA,sBAAA;EAAA,iBAAA;CvDovLH;AwD5wLD;EACE,iBAAA;CxD8wLD;AwD1wLD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;EACA,kCAAA;EAIA,WAAA;CxDywLD;AwDtwLC;EnDiHA,sCAAA;EACI,kCAAA;EACC,iCAAA;EACG,8BAAA;EAkER,oDAAA;EAEK,0CAAA;EACG,4CAAA;EAAA,oCAAA;EAAA,iGAAA;CLulLT;AwD5wLC;EnD6GA,mCAAA;EACI,+BAAA;EACC,8BAAA;EACG,2BAAA;CLkqLT;AwDhxLD;EACE,mBAAA;EACA,iBAAA;CxDkxLD;AwD9wLD;EACE,mBAAA;EACA,YAAA;EACA,aAAA;CxDgxLD;AwD5wLD;EACE,mBAAA;EACA,uBAAA;EACA,6BAAA;EACA,uBAAA;EACA,qCAAA;EACA,mBAAA;EnDcA,iDAAA;EACQ,yCAAA;EmDZR,WAAA;CxD8wLD;AwD1wLD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,uBAAA;CxD4wLD;AwD1wLC;ElCpEA,yBAAA;EACA,WAAA;CtBi1LD;AwD7wLC;ElCrEA,0BAAA;EACA,aAAA;CtBq1LD;AwD5wLD;EACE,cAAA;EACA,iCAAA;CxD8wLD;AwD1wLD;EACE,iBAAA;CxD4wLD;AwDxwLD;EACE,UAAA;EACA,wBAAA;CxD0wLD;AwDrwLD;EACE,mBAAA;EACA,cAAA;CxDuwLD;AwDnwLD;EACE,cAAA;EACA,kBAAA;EACA,8BAAA;CxDqwLD;AwDxwLD;EAQI,iBAAA;EACA,iBAAA;CxDmwLH;AwD5wLD;EAaI,kBAAA;CxDkwLH;AwD/wLD;EAiBI,eAAA;CxDiwLH;AwD5vLD;EACE,mBAAA;EACA,aAAA;EACA,YAAA;EACA,aAAA;EACA,iBAAA;CxD8vLD;AwD1vLD;EAEE;IACE,aAAA;IACA,kBAAA;GxD2vLD;EwDzvLD;InDrEA,kDAAA;IACQ,0CAAA;GLi0LP;EwDxvLD;IAAY,aAAA;GxD2vLX;CACF;AwDzvLD;EACE;IAAY,aAAA;GxD4vLX;CACF;AyD34LD;EACE,mBAAA;EACA,cAAA;EACA,eAAA;ECRA,4DAAA;EAEA,mBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,uBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,oBAAA;EDHA,gBAAA;EnCTA,yBAAA;EACA,WAAA;CtBm6LD;AyDv5LC;EnCbA,0BAAA;EACA,aAAA;CtBu6LD;AyD15LC;EACE,eAAA;EACA,iBAAA;CzD45LH;AyD15LC;EACE,eAAA;EACA,iBAAA;CzD45LH;AyD15LC;EACE,eAAA;EACA,gBAAA;CzD45LH;AyD15LC;EACE,eAAA;EACA,kBAAA;CzD45LH;AyDx5LC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,uBAAA;CzD05LH;AyDx5LC;EACE,WAAA;EACA,UAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;CzD05LH;AyDx5LC;EACE,UAAA;EACA,UAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;CzD05LH;AyDx5LC;EACE,SAAA;EACA,QAAA;EACA,iBAAA;EACA,4BAAA;EACA,yBAAA;CzD05LH;AyDx5LC;EACE,SAAA;EACA,SAAA;EACA,iBAAA;EACA,4BAAA;EACA,wBAAA;CzD05LH;AyDx5LC;EACE,OAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,0BAAA;CzD05LH;AyDx5LC;EACE,OAAA;EACA,WAAA;EACA,iBAAA;EACA,wBAAA;EACA,0BAAA;CzD05LH;AyDx5LC;EACE,OAAA;EACA,UAAA;EACA,iBAAA;EACA,wBAAA;EACA,0BAAA;CzD05LH;AyDr5LD;EACE,iBAAA;EACA,iBAAA;EACA,YAAA;EACA,mBAAA;EACA,uBAAA;EACA,mBAAA;CzDu5LD;AyDn5LD;EACE,mBAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;CzDq5LD;A2D9/LD;EACE,mBAAA;EACA,OAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;EACA,aAAA;EDXA,4DAAA;EAEA,mBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,uBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,oBAAA;ECAA,gBAAA;EACA,uBAAA;EACA,6BAAA;EACA,uBAAA;EACA,qCAAA;EACA,mBAAA;EtDiDA,kDAAA;EACQ,0CAAA;CL49LT;A2D1gMC;EAAQ,kBAAA;C3D6gMT;A2D5gMC;EAAU,kBAAA;C3D+gMX;A2D9gMC;EAAW,iBAAA;C3DihMZ;A2DhhMC;EAAS,mBAAA;C3DmhMV;A2D1iMD;EA4BI,mBAAA;C3DihMH;A2D/gMG;;EAEE,mBAAA;EACA,eAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;C3DihML;A2D9gMG;EACE,YAAA;EACA,mBAAA;C3DghML;A2D5gMC;EACE,cAAA;EACA,UAAA;EACA,mBAAA;EACA,0BAAA;EACA,sCAAA;EACA,uBAAA;C3D8gMH;A2D7gMG;EACE,YAAA;EACA,mBAAA;EACA,aAAA;EACA,uBAAA;EACA,uBAAA;C3D+gML;A2D5gMC;EACE,SAAA;EACA,YAAA;EACA,kBAAA;EACA,4BAAA;EACA,wCAAA;EACA,qBAAA;C3D8gMH;A2D7gMG;EACE,cAAA;EACA,UAAA;EACA,aAAA;EACA,yBAAA;EACA,qBAAA;C3D+gML;A2D5gMC;EACE,WAAA;EACA,UAAA;EACA,mBAAA;EACA,oBAAA;EACA,6BAAA;EACA,yCAAA;C3D8gMH;A2D7gMG;EACE,SAAA;EACA,mBAAA;EACA,aAAA;EACA,oBAAA;EACA,0BAAA;C3D+gML;A2D3gMC;EACE,SAAA;EACA,aAAA;EACA,kBAAA;EACA,sBAAA;EACA,2BAAA;EACA,uCAAA;C3D6gMH;A2D5gMG;EACE,WAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,wBAAA;C3D8gML;A2DzgMD;EACE,kBAAA;EACA,UAAA;EACA,gBAAA;EACA,0BAAA;EACA,iCAAA;EACA,2BAAA;C3D2gMD;A2DxgMD;EACE,kBAAA;C3D0gMD;A4D9nMD;EACE,mBAAA;C5DgoMD;A4D7nMD;EACE,mBAAA;EACA,YAAA;EACA,iBAAA;C5D+nMD;A4DloMD;EAMI,mBAAA;EACA,cAAA;EvD6KF,0CAAA;EACK,qCAAA;EACG,kCAAA;CLm9LT;A4DzoMD;;EAcM,eAAA;C5D+nML;A4D3nMG;EAAA;IvDuLF,uDAAA;IAEK,6CAAA;IACG,+CAAA;IAAA,uCAAA;IAAA,0GAAA;IA7JR,oCAAA;IAEQ,4BAAA;IA+GR,4BAAA;IAEQ,oBAAA;GLw/LP;E4DnoMG;;IvDmHJ,2CAAA;IACQ,mCAAA;IuDjHF,QAAA;G5DsoML;E4DpoMG;;IvD8GJ,4CAAA;IACQ,oCAAA;IuD5GF,QAAA;G5DuoML;E4DroMG;;;IvDyGJ,wCAAA;IACQ,gCAAA;IuDtGF,QAAA;G5DwoML;CACF;A4D9qMD;;;EA6CI,eAAA;C5DsoMH;A4DnrMD;EAiDI,QAAA;C5DqoMH;A4DtrMD;;EAsDI,mBAAA;EACA,OAAA;EACA,YAAA;C5DooMH;A4D5rMD;EA4DI,WAAA;C5DmoMH;A4D/rMD;EA+DI,YAAA;C5DmoMH;A4DlsMD;;EAmEI,QAAA;C5DmoMH;A4DtsMD;EAuEI,YAAA;C5DkoMH;A4DzsMD;EA0EI,WAAA;C5DkoMH;A4D1nMD;EACE,mBAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;EACA,YAAA;EACA,mBAAA;EACA,0CAAA;EACA,mCAAA;EtCpGA,0BAAA;EACA,aAAA;CtBiuMD;A4DxnMC;EdrGE,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,uHAAA;EACA,4BAAA;C9CguMH;A4D5nMC;EACE,SAAA;EACA,WAAA;Ed1GA,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,uHAAA;EACA,4BAAA;C9CyuMH;A4D9nMC;;EAEE,YAAA;EACA,sBAAA;EACA,WAAA;EtCxHF,0BAAA;EACA,aAAA;CtByvMD;A4DhqMD;;;;EAuCI,mBAAA;EACA,SAAA;EACA,WAAA;EACA,sBAAA;EACA,kBAAA;C5D+nMH;A4D1qMD;;EA+CI,UAAA;EACA,mBAAA;C5D+nMH;A4D/qMD;;EAoDI,WAAA;EACA,oBAAA;C5D+nMH;A4DprMD;;EAyDI,YAAA;EACA,aAAA;EACA,mBAAA;EACA,eAAA;C5D+nMH;A4D3nMG;EACE,iBAAA;C5D6nML;A4DznMG;EACE,iBAAA;C5D2nML;A4DjnMD;EACE,mBAAA;EACA,aAAA;EACA,UAAA;EACA,YAAA;EACA,WAAA;EACA,gBAAA;EACA,kBAAA;EACA,mBAAA;EACA,iBAAA;C5DmnMD;A4D5nMD;EAYI,sBAAA;EACA,YAAA;EACA,aAAA;EACA,YAAA;EACA,oBAAA;EACA,gBAAA;EAUA,0BAAA;EACA,mCAAA;EAEA,uBAAA;EACA,oBAAA;C5DymMH;A4DxoMD;EAmCI,YAAA;EACA,aAAA;EACA,UAAA;EACA,uBAAA;C5DwmMH;A4DjmMD;EACE,mBAAA;EACA,WAAA;EACA,aAAA;EACA,UAAA;EACA,YAAA;EACA,kBAAA;EACA,qBAAA;EACA,YAAA;EACA,mBAAA;EACA,0CAAA;C5DmmMD;A4DjmMC;EACE,kBAAA;C5DmmMH;A4D7lMD;EAGE;;;;IAKI,YAAA;IACA,aAAA;IACA,kBAAA;IACA,gBAAA;G5D4lMH;E4DpmMD;;IAYI,mBAAA;G5D4lMH;E4DxmMD;;IAgBI,oBAAA;G5D4lMH;E4DvlMD;IACE,WAAA;IACA,UAAA;IACA,qBAAA;G5DylMD;E4DrlMD;IACE,aAAA;G5DulMD;CACF;A6Dz1MC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,eAAA;EACA,aAAA;C7Dy3MH;A6Dv3MC;;;;;;;;;;;;;;;;EACE,YAAA;C7Dw4MH;AiC94MD;E6BVE,eAAA;EACA,mBAAA;EACA,kBAAA;C9D25MD;AiCh5MD;EACE,wBAAA;CjCk5MD;AiCh5MD;EACE,uBAAA;CjCk5MD;AiC14MD;EACE,yBAAA;CjC44MD;AiC14MD;EACE,0BAAA;CjC44MD;AiC14MD;EACE,mBAAA;CjC44MD;AiC14MD;E8BzBE,YAAA;EACA,mBAAA;EACA,kBAAA;EACA,8BAAA;EACA,UAAA;C/Ds6MD;AiCx4MD;EACE,yBAAA;CjC04MD;AiCn4MD;EACE,gBAAA;CjCq4MD;AgEt6MD;EACE,oBAAA;ChEw6MD;AgEl6MD;;;;EClBE,yBAAA;CjE07MD;AgEj6MD;;;;;;;;;;;;EAYE,yBAAA;ChEm6MD;AgE/5MC;EAAA;ICjDA,0BAAA;GjEo9MC;EiEn9MD;IAAU,0BAAA;GjEs9MT;EiEr9MD;IAAU,8BAAA;GjEw9MT;EiEv9MD;;IACU,+BAAA;GjE09MT;CACF;AgEz6MC;EAAA;IACE,0BAAA;GhE46MD;CACF;AgEz6MC;EAAA;IACE,2BAAA;GhE46MD;CACF;AgEz6MC;EAAA;IACE,iCAAA;GhE46MD;CACF;AgEx6MC;EAAA;ICtEA,0BAAA;GjEk/MC;EiEj/MD;IAAU,0BAAA;GjEo/MT;EiEn/MD;IAAU,8BAAA;GjEs/MT;EiEr/MD;;IACU,+BAAA;GjEw/MT;CACF;AgEl7MC;EAAA;IACE,0BAAA;GhEq7MD;CACF;AgEl7MC;EAAA;IACE,2BAAA;GhEq7MD;CACF;AgEl7MC;EAAA;IACE,iCAAA;GhEq7MD;CACF;AgEj7MC;EAAA;IC3FA,0BAAA;GjEghNC;EiE/gND;IAAU,0BAAA;GjEkhNT;EiEjhND;IAAU,8BAAA;GjEohNT;EiEnhND;;IACU,+BAAA;GjEshNT;CACF;AgE37MC;EAAA;IACE,0BAAA;GhE87MD;CACF;AgE37MC;EAAA;IACE,2BAAA;GhE87MD;CACF;AgE37MC;EAAA;IACE,iCAAA;GhE87MD;CACF;AgE17MC;EAAA;IChHA,0BAAA;GjE8iNC;EiE7iND;IAAU,0BAAA;GjEgjNT;EiE/iND;IAAU,8BAAA;GjEkjNT;EiEjjND;;IACU,+BAAA;GjEojNT;CACF;AgEp8MC;EAAA;IACE,0BAAA;GhEu8MD;CACF;AgEp8MC;EAAA;IACE,2BAAA;GhEu8MD;CACF;AgEp8MC;EAAA;IACE,iCAAA;GhEu8MD;CACF;AgEn8MC;EAAA;IC7HA,yBAAA;GjEokNC;CACF;AgEn8MC;EAAA;IClIA,yBAAA;GjEykNC;CACF;AgEn8MC;EAAA;ICvIA,yBAAA;GjE8kNC;CACF;AgEn8MC;EAAA;IC5IA,yBAAA;GjEmlNC;CACF;AgE77MD;ECvJE,yBAAA;CjEulND;AgE77MC;EAAA;IClKA,0BAAA;GjEmmNC;EiElmND;IAAU,0BAAA;GjEqmNT;EiEpmND;IAAU,8BAAA;GjEumNT;EiEtmND;;IACU,+BAAA;GjEymNT;CACF;AgEx8MD;EACE,yBAAA;ChE08MD;AgEx8MC;EAAA;IACE,0BAAA;GhE28MD;CACF;AgEz8MD;EACE,yBAAA;ChE28MD;AgEz8MC;EAAA;IACE,2BAAA;GhE48MD;CACF;AgE18MD;EACE,yBAAA;ChE48MD;AgE18MC;EAAA;IACE,iCAAA;GhE68MD;CACF;AgEz8MC;EAAA;ICrLA,yBAAA;GjEkoNC;CACF","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable\n\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// 1. Remove the bottom border in Chrome 57- and Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n//\n\nabbr[title] {\n border-bottom: none; // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type\n\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important; // Black prints faster: h5bp.com/s\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n}\n","// stylelint-disable value-list-comma-newline-after, value-list-comma-space-after, indentation, declaration-colon-newline-after, font-family-no-missing-generic-family-keyword\n\n//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"@{icon-font-path}@{icon-font-name}.eot\");\n src: url(\"@{icon-font-path}@{icon-font-name}.eot?#iefix\") format(\"embedded-opentype\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff2\") format(\"woff2\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff\") format(\"woff\"),\n url(\"@{icon-font-path}@{icon-font-name}.ttf\") format(\"truetype\"),\n url(\"@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}\") format(\"svg\");\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// https://getbootstrap.com/docs/3.4/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: https://a11yproject.com/posts/how-to-hide-content\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// stylelint-disable indentation, property-no-vendor-prefix, selector-no-vendor-prefix\n\n// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n word-wrap: break-word;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // WebKit-specific. Other browsers will keep their default outline style.\n // (Initially tried to also force default via `outline: initial`,\n // but that seems to erroneously remove the outline in Firefox altogether.)\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// stylelint-disable media-feature-name-no-vendor-prefix, media-feature-parentheses-space-inside, media-feature-name-no-unknown, indentation, at-rule-name-space-after\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","// stylelint-disable selector-list-comma-newline-after, selector-no-qualifying-type\n\n//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: 400;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n padding: .2em;\n background-color: @state-warning-bg;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: \"\\2014 \\00A0\"; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: \"\"; }\n &:after {\n content: \"\\00A0 \\2014\"; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n color: @pre-color;\n word-break: break-all;\n word-wrap: break-word;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n padding-right: ceil((@gutter / 2));\n padding-left: floor((@gutter / 2));\n margin-right: auto;\n margin-left: auto;\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-right: floor((@gutter / -2));\n margin-left: ceil((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-right: floor((@grid-gutter-width / 2));\n padding-left: ceil((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","// stylelint-disable selector-max-type, selector-max-compound-selectors, selector-no-qualifying-type\n\n//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n\n // Table cell sizing\n //\n // Reset default table behavior\n\n col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-column;\n float: none;\n }\n\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-cell;\n float: none;\n }\n }\n}\n\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\n\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n min-height: .01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n overflow-x: auto;\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * .75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type, property-no-vendor-prefix, media-feature-name-no-vendor-prefix\n\n//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: 700;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\ninput[type=\"search\"] {\n // Override content-box in Normalize (* isn't specific enough)\n .box-sizing(border-box);\n\n // Search inputs in iOS\n //\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n -webkit-appearance: none;\n appearance: none;\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n\n // Apply same disabled cursor tweak as for inputs\n // Some special care is needed because