Changes:
Changes:
- @xsoheilalizadeh - Fixed a metric name in #43 and some unit typos in #44
- @g3rv4 - Fixed
TaggedMetricFactoryconstructor params order in #45
Released for .NET Standard 2.0 and .NET Core 3.1
By explicitly targeting .NET Core 3.1 we are able to add support for configuring the MetricsCollector using the standard startup mechanisms in .NET Core.
We have changed how metrics are created to disconnect them from a MetricsCollector. Applications should now use MetricSource for creating custom metrics, which can be consumed without having a MetricsCollector instantiated.
There are also some built-in metric source implementations:
ProcessMetricSource- provides CPU time, paged memory, virtual memory and thread counts for the current processGarbageCollectionMetricSource- provides GC gen0, gen1 and gen2 collection metrics in .NET full frameworkAspNetMetricSource- provides request metrics for ASP.NET CoreRuntimeMetricSource- provides CPU, memory, GC and thread pool metrics in .NET Core
Metric sources can be configured using MetricsCollectorOptions.Sources in .NET full framework or the AddSource method on the IMetricsCollectorBuilder exposed by AddMetricsCollector on an IServiceCollection.
- Removal of
MetricGroupand sealing all built-in metrics. Tagged metrics can be created by using the relelvantAdd*methods on aMetricSource. - Removal of
CreateMetricWithoutPrefixandGetMetricWithoutPrefixmethods ; overrideMetricSource.Addto have a single place to apply prefixes - Removal of ctor with the exception handler parameter from
MetricsCollectorOptions- in order to support the options framework in .NET Core there is now just a default ctor. Use theUseExceptionHandleron theIMetricsCollectorBuilderor assign theExceptionHandlerproperty directly
Released for .NET Standard 2.0
Rename of project from BosunReporter to StackExchange.Metrics. Rename of namespaces and anything Bosun to just Metric or Metrics.
We now support reporting metrics to several handlers via an extensible mechanism. In the box we include handlers for Bosun, SignalFx and DataDog. As a result of this, some configuration options have been moved to the handler rather than to the collector itself.
Released for .NET Standard 2.0, and .NET Framework 4.5.
Many BosunReporter tasks run on background threads, where an uncaught exception will crash the process. BosunReporter previously allowed you to subscribe to an "OnBackgroundException" event to handle exceptions, but if you didn't, exceptions would be thrown and take down your process.
The exception handler is now required. Instead of an event, it's an Action<Exception> parameter on the BosunOptions constructor. If null, an exception is thrown.
var options = new BosunOptions(ex => LogException(ex)) { ... };
var collector = new MetricsCollector(options);BosunOptions.ReportingInterval was renamed to SnapshotInterval to better reflect what it does (it controls how frequently metrics are snapshotted - which isn't necessarily when they are reported to Bosun). Its type was also changed from int (seconds) to TimeSpan.
BosunOptions.MetaDataReportingInterval was removed. Metadata is now generated and sent to Bosun anytime new metrics have been created since the last snapshot. Metadata is also regenerated and sent approximately every 24 hours when no new metrics are being created. Metadata is sent during shutdown, if new metrics were created since the last snapshot.
- An Access Token can be provided via
BosunOptions.AccessTokenorBosunOptions.GetAccessToken. If not null or empty, this will be added as anX-Access-Tokenheader on all API requests. MetricGroup<T, TMetric>.PopulateFromEnum()now has an optionalincludeObsoleteparameter (defaults to true). If false, obsolete enum values will not be used to populated the metric group.- AggregateGauge:
- Is now abstract. You must inherit from it in order to use it.
- If a child class does not specify aggregators of its own (via
[GaugeAggregator]), it will inherit its parent's aggregators. If a child class does specify at least one aggregator, then it will not inherit any of its parent's aggregators.
- SnapshotCounter and SnapshotGauge:
- No longer have default protected constructors
- The
Func<T>argument is always required - The
Func<T>field is now private GetValueis now public
- Counter:
Valueis now a read-only property instead of a field. Inheriting classes can still write to the protected_valuefield, if necessary.
- Removed all metric interfaces (IDoubleGauge, ILongGauge, IIntGauge, IDoubleCounter, ILongCounter, IIntCounter). They really weren't all that useful. You should generally know what you're recording on explicitly. Using an interface probably represents an anti-pattern. If you really need an interface, you can define your own interface and metric types which implement them.
- Removed
MetricsCollector.BosunUrlandGetBosunUrlproperties. They were confusing since it wasn't obvious what the source of truth was. Use theTryGetBosunUrlandSetBosunUrlmethods if you need to get or update the Bosun endpoint URL after the MetricsCollector is created. AfterSerializationInfoandAfterPostInfo:MillisecondsDurationproperties were changed toDurationof typeTimeSpan.- Constructors are internal. They were never intended to be instantiated outside of BosunReporter.
- Removed
MetricsCollector.LastPostInfoandLastSerializationInfoproperties. UseAfterPostandAfterSerializationevents instead.
BosunPostException.StatusCodeis now nullable, since not all POST errors will have an HTTP status code returned.- Removed the Jil dependency (there are no other NuGet dependencies).
- Added XML documentation for all public types and methods.
- Fix floating-point formatting in certain cultures.
- Turned all BosunOptions fields into properties.
- Fixed
MetricsCollector.TotalMetricsPostedproperty. - Wait 10 seconds to retry failed POSTs, instead of 5 seconds. This may eventually be configurable or use a more sophisticated back-off.
- Stop metric serialization when the queue is full so that we only throw the queue-full exception once per serialization interval.
BosunOptions.ThrowOnPostFailnow only applies to 5xx response codes. So, if a 4xx is received, an exception will still be thrown since that probably indicates a problem with the library rather than Bosun being down.- Increased the default
BosunOptions.MaxPendingPayloadsfrom 120 to 240.
There are several changes in version 3.0, including some new features and minor breaking changes.
The way BosunReporter batches metrics to Bosun has changed substantially. Instead of having a batch size specified in metrics, the maximum batch size is now specified in bytes, which gives you better control over network traffic. However, this means that some BosunOptions had to change.
BatchSize(unit: metrics), has been replaced withMaxPayloadSize(unit: bytes). The default is 8000 bytes.MaxQueueLength(unit: metrics) has been replaced withMaxPendingPayloads(unit: payloads) which is the maximum number of payloads (each with a max size ofMaxPayloadSize) to queue for sending. Payloads are re-added to the queue when sending to Bosun fails, unless the queue is full. The default is 120 payloads.
[IgnoreDefaultBosunTags], which could be applied to a metric class in order to not inherit the default tags, has been removed and replaced by [ExcludeDefaultTags].
The new attribute can be used to either ignore all, or only some, of the default tags.
[ExcludeDefaultTags]
public class Metric1 : BosunMetric
{
// all default tags are excluded
}
[ExcludeDefaultTags("host", "tier")]
public class Metric2 : BosunMetric
{
// default tags "host" and "tier" are specifically excluded
}Keep in mind that all metrics (except external counters) must have at least one tag. So, if you exclude all default tags, you will need to add at least one of your own.
There is also a complementary attribute [RestoreDefaultTags] which can be used to restore some, or all, of the default tags. This can be useful if you inherit from a class which excludes default tags.
[RestoreDefaultTags("host")]
public class Metric3 : Metric1
{
// "host" tag will now be included, even though Metric1 excludes default tags
}This feature requires you to be using tsdbrelay as an intermediary between your app and Bosun. You'll need to run tsdbrelay with
-redis=REDIS_SERVER_NAMEand setup an scollector instance to scrape it with:[[RedisCounters]] Server = "localhost:6379" Database = 2
External counters are intended to solve the problem of counting low-volume events.
The nature of a low-volume counter is that its per-second rate is going to be zero most of the time. For example:
If you could simply see the start and end values for a given time interval, you would have a better sense of how frequent the events are. But, unfortunately, a normal Bosun counter resets every time the application restarts, so you end up with a graph that might look something like this when viewed as a gauge:
To solve this problem, external counters are persistent (the value doesn't reset every time the app restarts). Tsdbrelay stores the value of the counter in Redis, and BosunReporter sends it increments when an event happens. Tsdbrelay then periodically reports the metric to Bosun.
This means that when you graph the metric as a gauge, it will always be going up, and you can easily see start and end values for any time interval.
Remember, these are for LOW VOLUME events. If you expect more than one event per minute across all instances of your app, you should use a normal counter.
You can enable/disable sending external counter increments using BosunOptions.EnableExternalCounters during initialization, or by changing MetricsCollector.EnableExternalCounters at runtime.
The usage of ExternalCounter is exactly the same as Counter except that you can only increment by 1.
var counter = collector.CreateMetric<ExternalCounter>("ext_counter", "units", "description");
counter.Increment();You can also inherit from ExternalCounter in order to add tags (like any other metric type).
tsdbrelay will automatically add the "host" tag. This means that metrics which inherit from ExternalCounter are not required to have any tags. ExternalCounter excludes the "host" tag by default for the same reason.
Custom metric classes are classes which inherit directly from
BosunMetric. If you only inherit from the built-in metric types (e.g.Counter,AggregateGauge, etc.) then you don't need to do anything.
There are some breaking changes in how custom metric classes are implemented.
The signature of BosunMetric.Serialize has changed from IEnumerable<string> (string unixTimestamp) to void (MetricWriter writer, DateTime now). Instead of returning an enumeration of strings representing the serialized metrics, you call a protected method WriteValue() for each metric you want to serialize.
For example:
protected override void Serialize(MetricWriter writer, DateTime now)
{
WriteValue(writer, Value, now);
}WriteValue takes an optional fourth parameter which is the suffix index (defaults to zero). See "Suffixes" below for more information on the meaning of the index.
WriteValue must only be called from inside the Serialize method, and should should not store a reference to the MetricWriter.
The Serialize method should not be used to perform computationally expensive work because it is called in serial with all other metrics. Instead, expensive work should be performed in PreSerialize, which is called in parallel with other metrics that implement it.
For example, AggregateGauge uses PreSerialize to perform sorting and aggregation operations, and then it stores a snapshot which the Serialize method reads from.
protected override void PreSerialize()
{
// computationally expensive stuff here
}BosunReporter has always supported multiple suffixes per metric. This is what allows AggregateGauge to serialize into several metrics. However, the implementation has changed slightly.
IEnumerable<string> GetSuffixes() has been replaced with string[] GetImmutableSuffixesArray(). As the name implies, the suffixes must be immutable for the lifetime of the metric.
Also, instead of passing around the string representation of the suffix, indexes into the suffix array are used (such as in the WriteValue() and GetDescription() methods).
As described above, the GetDescription() method now takes int suffixIndex instead of string suffix. This is an index into the array returned by GetImmutableSuffixesArray().

