diff --git a/Source/common/BUILD b/Source/common/BUILD index 6d0997a66..0bf40c88a 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -188,6 +188,12 @@ objc_library( ], ) +objc_library( + name = "SNTMetricSet", + srcs = ["SNTMetricSet.m"], + hdrs = ["SNTMetricSet.h"], +) + objc_library( name = "SNTXPCSyncdInterface", srcs = ["SNTXPCSyncdInterface.m"], @@ -241,5 +247,11 @@ santa_unit_test( santa_unit_test( name = "SNTPrefixTreeTest", srcs = ["SNTPrefixTreeTest.mm"], - deps = ["SNTPrefixTree"], + deps = [":SNTPrefixTree"], +) + +santa_unit_test( + name = "SNTMetricSetTest", + srcs = ["SNTMetricSetTest.m"], + deps = [":SNTMetricSet"], ) diff --git a/Source/common/SNTMetricSet.h b/Source/common/SNTMetricSet.h new file mode 100644 index 000000000..158f974ee --- /dev/null +++ b/Source/common/SNTMetricSet.h @@ -0,0 +1,173 @@ +/// Copyright 2021 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import + +/** + * Provides an abstraction for various metric systems that will be exported to + * monitoring systems via the MetricService. This is used to store internal + * counters and metrics that can be exported to an external monitoring system. + * + * `SNTMetricSet` for storing and creating metrics and counters. This is + * the externally visible interface + * class. + * + * Metric classes: + * * `SNTMetric` to store metric values broken down by "field" dimensions. + * * subclasses of `SNTMetric` with suitable setters: + * * `SNTMetricCounter` + * * `SNTMetricGaugeInt64` + * * `SNTMetricGaugeDouble` + * * `SNTMetricString` + * * `SNTMetricBool` + */ + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, SNTMetricType) { + SNTMetricTypeUnknown = 0, + SNTMetricTypeConstantBool = 1, + SNTMetricTypeConstantString = 2, + SNTMetricTypeConstantInt64 = 3, + SNTMetricTypeConstantDouble = 4, + SNTMetricTypeGaugeBool = 5, + SNTMetricTypeGaugeString = 6, + SNTMetricTypeGaugeInt64 = 7, + SNTMetricTypeGaugeDouble = 8, + SNTMetricTypeCounter = 9, +}; + +@interface SNTMetric : NSObject +- (NSDictionary *)export; +@end + +@interface SNTMetricCounter : SNTMetric +- (void)incrementBy:(long long)step forFieldValues:(NSArray *)fieldValues; +- (void)incrementForFieldValues:(NSArray *)fieldValues; +- (long long)getCountForFieldValues:(NSArray *)fieldValues; +@end + +@interface SNTMetricInt64Gauge : SNTMetric +- (void)set:(long long)value forFieldValues:(NSArray *)fieldValues; +- (long long)getGaugeValueForFieldValues:(NSArray *)fieldValues; +@end + +@interface SNTMetricDoubleGauge : SNTMetric +- (void)set:(double)value forFieldValues:(NSArray *)fieldValues; +- (double)getGaugeValueForFieldValues:(NSArray *)fieldValues; +@end + +@interface SNTMetricStringGauge : SNTMetric +- (void)set:(NSString *)value forFieldValues:(NSArray *)fieldValues; +- (NSString *)getStringValueForFieldValues:(NSArray *)fieldValues; +@end + +@interface SNTMetricBooleanGauge : SNTMetric +- (void)set:(BOOL)value forFieldValues:(NSArray *)fieldValues; +- (BOOL)getBoolValueForFieldValues:(NSArray *)fieldValues; +@end + +/** + * A registry of metrics with associated fields. + */ +@interface SNTMetricSet : NSObject +- (instancetype)initWithHostname:(NSString *)hostname username:(NSString *)username; + +/* Returns a counter with the given name, field names and help + * text, registered with the MetricSet. + * + * @param name The counter name, for example @"/proc/cpu". + * @param fieldNames The counter's field names, for example @[@"result"]. + * @param helpText The counter's help description. + * @return A counter with the given specification registered with this root. + * The returned counter might have been created earlier with the same + * specification. + * @throw NSInternalInconsistencyException When trying to register a second + * counter with the same name but a different schema as an existing one + */ +- (SNTMetricCounter *)counterWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)text; + +- (void)addRootLabel:(NSString *)label value:(NSString *)value; + +/** + * Returns a int64 gauge metric with the given Streamz name and help text, + * registered with this MetricSet. + * + * @param name The metric name, for example @"/memory/free". + * @param fieldNames The metric's field names, for example @[@"type"]. + * @param helpText The metric's help description. + */ +- (SNTMetricInt64Gauge *)int64GaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText; + +/** + * Returns a double gauge metric with the given name and help text, + * registered with this root. + * + * @param name The metric name, for example @"/memory/free". + * @param fieldNames The metric's field names, for example @[@"type"]. + * @param helpText The metric's help description. + */ +- (SNTMetricDoubleGauge *)doubleGaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText; + +/** + * Returns a string gauge metric with the given name and help text, + * registered with this metric set. + * + * @param name The metric name, for example @"/santa/mode". + * @param fieldNames The metric's field names, for example @[@"type"]. + * @param helpText The metric's help description. + */ +- (SNTMetricStringGauge *)stringGaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText; + +/** + * Returns a boolean gauge metric with the given name and help text, + * registered with this metric set. + * + * @param name The metric name, for example @"/memory/free". + * @param fieldNames The metric's field names, for example @[@"type"]. + * @param helpText The metric's help description. + */ +- (SNTMetricBooleanGauge *)booleanGaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText; + +/** Creates a constant metric with a string value and no fields. */ +- (void)addConstantStringWithName:(NSString *)name + helpText:(NSString *)helpText + value:(NSString *)value; + +/** Creates a constant metric with an integer value and no fields. */ +- (void)addConstantIntegerWithName:(NSString *)name + helpText:(NSString *)helpText + value:(long long)value; + +/** Creates a constant metric with an integer value and no fields. */ +- (void)addConstantBooleanWithName:(NSString *)name helpText:(NSString *)helpText value:(BOOL)value; + +/** Register a callback to get executed just before each export. */ +- (void)registerCallback:(void (^)(void))callback; + +/** Export creates an NSDictionary of the state of the metrics */ +- (NSDictionary *)export; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/common/SNTMetricSet.m b/Source/common/SNTMetricSet.m new file mode 100644 index 000000000..0a90e437a --- /dev/null +++ b/Source/common/SNTMetricSet.m @@ -0,0 +1,599 @@ +/// Copyright 2021 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "SNTMetricSet.h" + +/** + * SNTMetricValue encapsulates the value of a metric along with the creation + * and update timestamps. It is thread-safe and has a separate field for each + * metric type. + * + * It is intended to only be used by SNTMetrics; + */ +@interface SNTMetricValue : NSObject +/** Increment the counter by the step value, updating timestamps appropriately. */ +- (void)addInt64:(long long)step; + +/** Set the Int64 value. */ +- (void)setInt64:(long long)value; + +/** Set the double value. */ +- (void)setDouble:(double)value; + +/** Set the string value. */ +- (void)setString:(NSString *)value; + +/** Set the BOOL string value. */ +- (void)setBool:(BOOL)value; + +/** + * Clears the last update timestamp. + * + * This makes the metric value always emit the current timestamp as last update timestamp. + */ +- (void)clearLastUpdateTimestamp; + +/** Getters */ +- (long long)getInt64Value; +- (double)getDoubleValue; +- (NSString *)getStringValue; +@end + +@implementation SNTMetricValue { + /** The int64 value for the SNTMetricValue, if set. */ + long long _int64Value; + + /** The double value for the SNTMetricValue, if set. */ + double _doubleValue; + + /** The string value for the SNTMetricValue, if set. */ + NSString *_stringValue; + + /** The boolean value for the SNTMetricValue, if set. */ + BOOL _boolValue; + + /** The first time this cell got created in the current process. */ + NSDate *_creationTime; + + /** The last time that the counter value was changed. */ + NSDate *_lastUpdate; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _creationTime = [NSDate date]; + _lastUpdate = _creationTime; + } + return self; +} + +- (void)addInt64:(long long)step { + @synchronized(self) { + _int64Value += step; + _lastUpdate = [NSDate date]; + } +} + +- (void)setInt64:(long long)value { + @synchronized(self) { + _int64Value = value; + _lastUpdate = [NSDate date]; + } +} + +- (long long)getInt64Value { + @synchronized(self) { + return _int64Value; + } +} + +- (void)setDouble:(double)value { + @synchronized(self) { + _doubleValue = value; + _lastUpdate = [NSDate date]; + } +} + +- (double)getDoubleValue { + @synchronized(self) { + return _doubleValue; + } +} + +- (void)setString:(NSString *)value { + @synchronized(self) { + _stringValue = [value copy]; + _lastUpdate = [NSDate date]; + } +} + +- (NSString *)getStringValue { + @synchronized(self) { + return [_stringValue copy]; + } +} + +- (void)setBool:(BOOL)value { + @synchronized(self) { + _boolValue = value; + _lastUpdate = [NSDate date]; + } +} + +- (BOOL)getBoolValue { + @synchronized(self) { + return _boolValue; + } +} + +- (void)clearLastUpdateTimestamp { + @synchronized(self) { + _lastUpdate = nil; + } +} + +- (NSDate *)getLastUpdatedTimestamp { + NSDate *updated = nil; + @synchronized(self) { + updated = [_lastUpdate copy]; + } + return updated; +} + +- (NSDate *)getCreatedTimestamp { + NSDate *created = nil; + @synchronized(self) { + created = [_creationTime copy]; + } + return created; +} +@end + +@implementation SNTMetric { + @private + /** Fully qualified metric name e.g. /ops/security/santa. */ + NSString *_name; + /** A help text for the metric to be exported to be exported. **/ + NSString *_help; + + /** Sorted list of the fieldNames **/ + NSArray *_fieldNames; + /** Mapping of field values to actual metric values (e.g. metric /proc/cpu_usage @"mode"=@"user" + * -> 0.89 */ + NSMutableDictionary *, SNTMetricValue *> *_metricsForFieldValues; + /** the type of metric this is e.g. counter, gauge etc. **/ + SNTMetricType _type; +} + +- (instancetype)initWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)help + type:(SNTMetricType)type { + self = [super init]; + if (self) { + _name = [name copy]; + _help = [help copy]; + _fieldNames = [fieldNames copy]; + _metricsForFieldValues = [[NSMutableDictionary alloc] init]; + _type = type; + } + return self; +} + +- (NSString *)name { + return _name; +} + +- (BOOL)hasSameSchemaAsMetric:(SNTMetric *)other { + if (![other isKindOfClass:[self class]]) { + return NO; + } + return [_name isEqualToString:other->_name] && [_help isEqualToString:other->_help] && + [_fieldNames isEqualTo:other->_fieldNames] && _type == other->_type; +} + +/** Retrieves the SNTMetricValue for a given field value. + Creates a new SNTMetricValue if none is present. */ +- (SNTMetricValue *)metricValueForFieldValues:(NSArray *)fieldValues { + NSParameterAssert(fieldValues.count == _fieldNames.count); + SNTMetricValue *metricValue = nil; + @synchronized(self) { + metricValue = _metricsForFieldValues[fieldValues]; + + if (!metricValue) { + // Deep copy to prevent mutations to the keys we store in the dictionary. + fieldValues = [fieldValues copy]; + metricValue = [[SNTMetricValue alloc] init]; + _metricsForFieldValues[fieldValues] = metricValue; + } + } + + return metricValue; +} + +- (NSDictionary *)encodeMetricValueForFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = _metricsForFieldValues[fieldValues]; + + NSMutableDictionary *fieldDict = [[NSMutableDictionary alloc] init]; + + fieldDict[@"created"] = [metricValue getCreatedTimestamp]; + fieldDict[@"last_updated"] = [metricValue getLastUpdatedTimestamp]; + fieldDict[@"value"] = [fieldValues componentsJoinedByString:@","]; + + switch (_type) { + case SNTMetricTypeConstantBool: + case SNTMetricTypeGaugeBool: + fieldDict[@"data"] = [NSNumber numberWithBool:[metricValue getBoolValue]]; + break; + case SNTMetricTypeConstantInt64: + case SNTMetricTypeCounter: + case SNTMetricTypeGaugeInt64: + fieldDict[@"data"] = [NSNumber numberWithLongLong:[metricValue getInt64Value]]; + break; + case SNTMetricTypeConstantDouble: + case SNTMetricTypeGaugeDouble: + fieldDict[@"data"] = [NSNumber numberWithDouble:[metricValue getDoubleValue]]; + break; + case SNTMetricTypeConstantString: + case SNTMetricTypeGaugeString: fieldDict[@"data"] = [metricValue getStringValue]; break; + default: break; + } + return fieldDict; +} + +- (NSDictionary *)export { + NSMutableDictionary *metricDict = [NSMutableDictionary dictionaryWithCapacity:_fieldNames.count]; + metricDict[@"type"] = [NSNumber numberWithInt:(int)_type]; + metricDict[@"fields"] = [[NSMutableDictionary alloc] init]; + + if (_fieldNames.count == 0) { + metricDict[@"fields"][@""] = @[ [self encodeMetricValueForFieldValues:@[]] ]; + } else { + for (NSString *fieldName in _fieldNames) { + NSMutableArray *fieldVals = [[NSMutableArray alloc] init]; + + for (NSArray *fieldValues in _metricsForFieldValues) { + [fieldVals addObject:[self encodeMetricValueForFieldValues:fieldValues]]; + } + + metricDict[@"fields"][fieldName] = fieldVals; + } + } + return metricDict; +} +@end + +@implementation SNTMetricCounter + +- (instancetype)initWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + return [super initWithName:name + fieldNames:fieldNames + helpText:helpText + type:SNTMetricTypeCounter]; +} + +- (void)incrementBy:(long long)step forFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + + if (!metricValue) { + return; + } + [metricValue addInt64:step]; +} + +- (void)incrementForFieldValues:(NSArray *)fieldValues { + [self incrementBy:1 forFieldValues:fieldValues]; +} + +- (long long)getCountForFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + + if (!metricValue) { + return -1; + } + + return [metricValue getInt64Value]; +} +@end + +@implementation SNTMetricInt64Gauge +- (instancetype)initWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + return [super initWithName:name + fieldNames:fieldNames + helpText:helpText + type:SNTMetricTypeGaugeInt64]; +} + +- (void)set:(long long)value forFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + [metricValue setInt64:value]; +} + +- (long long)getGaugeValueForFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + + if (!metricValue) { + return -1; + } + + return [metricValue getInt64Value]; +} +@end + +@implementation SNTMetricDoubleGauge + +- (instancetype)initWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)text { + return [super initWithName:name + fieldNames:fieldNames + helpText:text + type:SNTMetricTypeGaugeDouble]; +} + +- (void)set:(double)value forFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + [metricValue setDouble:value]; +} + +- (double)getGaugeValueForFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + + if (!metricValue) { + return -1; + } + + return [metricValue getDoubleValue]; +} +@end + +@implementation SNTMetricStringGauge +- (instancetype)initWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)text { + return [super initWithName:name + fieldNames:fieldNames + helpText:text + type:SNTMetricTypeGaugeString]; +} + +- (void)set:(NSString *)value forFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + [metricValue setString:value]; +} + +- (NSString *)getStringValueForFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + + if (!metricValue) { + return nil; + } + + return [metricValue getStringValue]; +} +@end + +@implementation SNTMetricBooleanGauge +- (instancetype)initWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + return [super initWithName:name + fieldNames:fieldNames + helpText:helpText + type:SNTMetricTypeGaugeBool]; +} + +- (void)set:(BOOL)value forFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + [metricValue setBool:value]; +} + +- (BOOL)getBoolValueForFieldValues:(NSArray *)fieldValues { + SNTMetricValue *metricValue = [self metricValueForFieldValues:fieldValues]; + + if (!metricValue) { + return false; + } + + return [metricValue getBoolValue]; +} +@end + +/** + * SNTMetricSet is the top level container for all metrics and metrics value + * its is abstracted from specific implementations but is close to Google's + * Monarch and Prometheus formats. + */ +@implementation SNTMetricSet { + @private + /** Labels that are used to identify the entity to that all metrics apply to. */ + NSMutableDictionary *_rootLabels; + /** Registered metrics keyed by name */ + NSMutableDictionary *_metrics; + + /** Callbacks to update metric values before exporting metrics */ + NSMutableArray *_callbacks; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _rootLabels = [[NSMutableDictionary alloc] init]; + _metrics = [[NSMutableDictionary alloc] init]; + _callbacks = [[NSMutableArray alloc] init]; + } + return self; +} + +- (instancetype)initWithHostname:(NSString *)hostname username:(NSString *)username { + self = [super init]; + if (self) { + _rootLabels = [[NSMutableDictionary alloc] init]; + _metrics = [[NSMutableDictionary alloc] init]; + _callbacks = [[NSMutableArray alloc] init]; + + _rootLabels[@"hostname"] = [hostname copy]; + _rootLabels[@"username"] = [username copy]; + } + + return self; +} + +- (void)addRootLabel:(NSString *)label value:(NSString *)value { + @synchronized(self) { + _rootLabels[label] = value; + } +} + +- (SNTMetric *)registerMetric:(nonnull SNTMetric *)metric { + @synchronized(self) { + SNTMetric *oldMetric = _metrics[[metric name]]; + if ([oldMetric hasSameSchemaAsMetric:metric]) { + return oldMetric; + } + NSAssert(!oldMetric, @"metric registered twice: %@", metric.name); + _metrics[metric.name] = metric; + } + return metric; +} + +- (void)registerCallback:(void (^)(void))callback { + @synchronized(self) { + [_callbacks addObject:callback]; + } +} + +- (SNTMetricCounter *)counterWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + SNTMetricCounter *c = [[SNTMetricCounter alloc] initWithName:name + fieldNames:fieldNames + helpText:helpText]; + [self registerMetric:c]; + return c; +} + +- (SNTMetricInt64Gauge *)int64GaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + SNTMetricInt64Gauge *g = [[SNTMetricInt64Gauge alloc] initWithName:name + fieldNames:fieldNames + helpText:helpText]; + [self registerMetric:g]; + return g; +} + +- (SNTMetricDoubleGauge *)doubleGaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + SNTMetricDoubleGauge *g = [[SNTMetricDoubleGauge alloc] initWithName:name + fieldNames:fieldNames + helpText:helpText]; + + [self registerMetric:g]; + return g; +} + +- (SNTMetricStringGauge *)stringGaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + SNTMetricStringGauge *s = [[SNTMetricStringGauge alloc] initWithName:name + fieldNames:fieldNames + helpText:helpText]; + + [self registerMetric:s]; + return s; +} + +- (SNTMetricBooleanGauge *)booleanGaugeWithName:(NSString *)name + fieldNames:(NSArray *)fieldNames + helpText:(NSString *)helpText { + SNTMetricBooleanGauge *b = [[SNTMetricBooleanGauge alloc] initWithName:name + fieldNames:fieldNames + helpText:helpText]; + + [self registerMetric:b]; + return b; +} + +- (void)addConstantStringWithName:(NSString *)name + helpText:(NSString *)helpText + value:(NSString *)value { + SNTMetric *metric = [[SNTMetric alloc] initWithName:name + fieldNames:@[] + helpText:helpText + type:SNTMetricTypeConstantString]; + + SNTMetricValue *metricValue = [metric metricValueForFieldValues:@[]]; + [metricValue setString:value]; + [self registerMetric:metric]; +} + +- (void)addConstantIntegerWithName:(NSString *)name + helpText:(NSString *)helpText + value:(long long)value { + SNTMetric *metric = [[SNTMetric alloc] initWithName:name + fieldNames:@[] + helpText:helpText + type:SNTMetricTypeConstantInt64]; + + SNTMetricValue *metricValue = [metric metricValueForFieldValues:@[]]; + [metricValue setInt64:value]; + [self registerMetric:metric]; +} + +- (void)addConstantBooleanWithName:(NSString *)name + helpText:(NSString *)helpText + value:(BOOL)value { + SNTMetric *metric = [[SNTMetric alloc] initWithName:name + fieldNames:@[] + helpText:helpText + type:SNTMetricTypeConstantBool]; + + SNTMetricValue *metricValue = [metric metricValueForFieldValues:@[]]; + [metricValue setBool:value]; + [self registerMetric:metric]; +} + +/** Export current state of the SNTMetricSet as an NSDictionary. */ +- (NSDictionary *)export { + NSDictionary *exported = nil; + + // Invoke callbacks to ensure metrics are up to date. + for (void (^cb)(void) in _callbacks) { + cb(); + } + + @synchronized(self) { + // Walk root labels + NSMutableDictionary *exportDict = [[NSMutableDictionary alloc] init]; + exportDict[@"root_labels"] = [NSDictionary dictionaryWithDictionary:_rootLabels]; + exportDict[@"metrics"] = [[NSMutableDictionary alloc] init]; + + // sort the metrics so we always get the same output. + for (id metricName in _metrics) { + SNTMetric *metric = [_metrics objectForKey:metricName]; + exportDict[@"metrics"][metricName] = [metric export]; + } + + exported = [NSDictionary dictionaryWithDictionary:exportDict]; + } + return exported; +} +@end diff --git a/Source/common/SNTMetricSetTest.m b/Source/common/SNTMetricSetTest.m new file mode 100644 index 000000000..4003b5390 --- /dev/null +++ b/Source/common/SNTMetricSetTest.m @@ -0,0 +1,526 @@ +#import + +#import "Source/common/SNTMetricSet.h" + +@interface SNTMetricCounterTest : XCTestCase +@end + +@interface SNTMetricGaugeInt64Test : XCTestCase +@end + +@interface SNTMetricDoubleGaugeTest : XCTestCase +@end + +@interface SNTMetricBooleanGaugeTest : XCTestCase +@end + +@interface SNTMetricStringGaugeTest : XCTestCase +@end + +@interface SNTMetricSetTest : XCTestCase +@end + +// Stub out NSDate's date method +@implementation NSDate (custom) + ++ (instancetype)date { + NSDateFormatter *formatter = NSDateFormatter.new; + [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ssZZZ"]; + return [formatter dateFromString:@"2021-08-05 13:00:10+0000"]; +} + +@end + +@implementation SNTMetricCounterTest +- (void)testSimpleCounter { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricCounter *c = + [metricSet counterWithName:@"/santa/events" + fieldNames:@[ @"rule_type" ] + helpText:@"Count of exec events broken out by rule type."]; + + XCTAssertNotNil(c, @"Expected returned SNTMetricCounter to not be nil"); + [c incrementForFieldValues:@[ @"certificate" ]]; + XCTAssertEqual(1, [c getCountForFieldValues:@[ @"certificate" ]], + @"Counter not incremendted by 1"); + [c incrementBy:3 forFieldValues:@[ @"certificate" ]]; + XCTAssertEqual(4, [c getCountForFieldValues:@[ @"certificate" ]], + @"Counter not incremendted by 3"); +} + +- (void)testExportNSDictionary { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricCounter *c = + [metricSet counterWithName:@"/santa/events" + fieldNames:@[ @"rule_type" ] + helpText:@"Count of exec events broken out by rule type."]; + + XCTAssertNotNil(c); + [c incrementForFieldValues:@[ @"certificate" ]]; + + NSDictionary *expected = @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeCounter], + @"fields" : @{ + @"rule_type" : @[ @{ + @"value" : @"certificate", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:1] + } ] + } + }; + + XCTAssertEqualObjects([c export], expected); +} +@end + +@implementation SNTMetricBooleanGaugeTest +- (void)testSimpleGauge { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricBooleanGauge *b = [metricSet booleanGaugeWithName:@"/santa/daemon_connected" + fieldNames:@[] + helpText:@"Is the daemon connected."]; + XCTAssertNotNil(b); + [b set:true forFieldValues:@[]]; + XCTAssertTrue([b getBoolValueForFieldValues:@[]]); + [b set:false forFieldValues:@[]]; + XCTAssertFalse([b getBoolValueForFieldValues:@[]]); +} + +- (void)testExportNSDictionary { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricBooleanGauge *b = [metricSet booleanGaugeWithName:@"/santa/daemon_connected" + fieldNames:@[] + helpText:@"Is the daemon connected."]; + XCTAssertNotNil(b); + [b set:true forFieldValues:@[]]; + NSDictionary *expected = @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeBool], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithBool:true] + } ] + } + }; + + NSDictionary *output = [b export]; + XCTAssertEqualObjects(output, expected); +} +@end + +@implementation SNTMetricGaugeInt64Test +- (void)testSimpleGauge { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricInt64Gauge *g = + [metricSet int64GaugeWithName:@"/santa/rules" + fieldNames:@[ @"rule_type" ] + helpText:@"Count of rules broken out by rule type."]; + + XCTAssertNotNil(g, @"Expected returned SNTMetricGaugeInt64 to not be nil"); + // set from zero + [g set:250 forFieldValues:@[ @"binary" ]]; + XCTAssertEqual(250, [g getGaugeValueForFieldValues:@[ @"binary" ]]); + + // Increase the gauge + [g set:500 forFieldValues:@[ @"binary" ]]; + XCTAssertEqual(500, [g getGaugeValueForFieldValues:@[ @"binary" ]]); + // Decrease after increase + [g set:100 forFieldValues:@[ @"binary" ]]; + XCTAssertEqual(100, [g getGaugeValueForFieldValues:@[ @"binary" ]]); + // Increase after decrease + [g set:750 forFieldValues:@[ @"binary" ]]; + XCTAssertEqual(750, [g getGaugeValueForFieldValues:@[ @"binary" ]]); + // TODO: export the tree to JSON and confirm the structure is correct. +} + +- (void)testExportNSDictionary { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricInt64Gauge *g = + [metricSet int64GaugeWithName:@"/santa/rules" + fieldNames:@[ @"rule_type" ] + helpText:@"Count of rules broken out by rule type."]; + + XCTAssertNotNil(g, @"Expected returned SNTMetricGaugeInt64 to not be nil"); + // set from zero + [g set:250 forFieldValues:@[ @"binary" ]]; + XCTAssertEqual(250, [g getGaugeValueForFieldValues:@[ @"binary" ]]); + + NSDictionary *expected = @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64], + @"fields" : @{ + @"rule_type" : @[ @{ + @"value" : @"binary", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:250] + } ] + } + }; + + XCTAssertEqualObjects([g export], expected); +} +@end + +@implementation SNTMetricDoubleGaugeTest + +- (void)testSimpleGauge { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricDoubleGauge *g = [metricSet doubleGaugeWithName:@"/proc/cpu_usage" + fieldNames:@[ @"mode" ] + helpText:@"CPU time consumed by this process."]; + + XCTAssertNotNil(g, @"Expected returned SNTMetricDoubleGauge to not be nil"); + // set from zero + [g set:(double)0.45 forFieldValues:@[ @"user" ]]; + XCTAssertEqual(0.45, [g getGaugeValueForFieldValues:@[ @"user" ]]); + + // Increase the gauge + [g set:(double)0.90 forFieldValues:@[ @"user" ]]; + XCTAssertEqual(0.90, [g getGaugeValueForFieldValues:@[ @"user" ]]); + // Decrease after increase + [g set:0.71 forFieldValues:@[ @"user" ]]; + XCTAssertEqual(0.71, [g getGaugeValueForFieldValues:@[ @"user" ]]); + // Increase after decrease + [g set:0.75 forFieldValues:@[ @"user" ]]; + XCTAssertEqual(0.75, [g getGaugeValueForFieldValues:@[ @"user" ]]); +} + +- (void)testExportNSDictionary { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricDoubleGauge *g = [metricSet doubleGaugeWithName:@"/proc/cpu_usage" + fieldNames:@[ @"mode" ] + helpText:@"CPU time consumed by this process."]; + + XCTAssertNotNil(g, @"Expected returned SNTMetricDoubleGauge to not be nil"); + // set from zero + [g set:(double)0.45 forFieldValues:@[ @"user" ]]; + [g set:(double)0.90 forFieldValues:@[ @"system" ]]; + + NSDictionary *expected = @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeDouble], + @"fields" : @{ + @"mode" : @[ + @{ + @"value" : @"user", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithDouble:0.45] + }, + @{ + @"value" : @"system", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithDouble:0.90] + } + ] + } + }; + XCTAssertEqualObjects([g export], expected); +} +@end + +@implementation SNTMetricStringGaugeTest +- (void)testSimpleGauge { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricStringGauge *s = [metricSet stringGaugeWithName:@"/santa/mode" + fieldNames:@[] + helpText:@"String description of the mode."]; + + XCTAssertNotNil(s); + [s set:@"testValue" forFieldValues:@[]]; + XCTAssertEqualObjects([s getStringValueForFieldValues:@[]], @"testValue"); +} +- (void)testExportNSDictionary { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricStringGauge *s = [metricSet stringGaugeWithName:@"/santa/mode" + fieldNames:@[] + helpText:@"String description of the mode."]; + + XCTAssertNotNil(s); + [s set:@"testValue" forFieldValues:@[]]; + + NSDictionary *expected = @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeString], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : @"testValue" + } ] + } + }; + + XCTAssertEqualObjects([s export], expected); +} +@end + +@implementation SNTMetricSetTest +- (void)testRootLabels { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + [metricSet addRootLabel:@"hostname" value:@"localhost"]; + + NSDictionary *expected = @{@"root_labels" : @{@"hostname" : @"localhost"}, @"metrics" : @{}}; + + NSDictionary *output = [metricSet export]; + XCTAssertEqualObjects(output, expected); +} + +- (void)testDoubleRegisteringIncompatibleMetricsFails { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + SNTMetricCounter *c = [metricSet counterWithName:@"/foo/bar" + fieldNames:@[ @"field" ] + helpText:@"lorem ipsum"]; + + XCTAssertNotNil(c); + XCTAssertThrows([metricSet counterWithName:@"/foo/bar" + fieldNames:@[ @"incompatible" ] + helpText:@"A little help text"], + @"Should raise error for incompatible field names"); + + XCTAssertThrows([metricSet counterWithName:@"/foo/bar" + fieldNames:@[ @"result" ] + helpText:@"INCOMPATIBLE"], + @"Should raise error for incompatible help text"); +} + +- (void)testRegisterCallback { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + // Register a callback metric which increments by one before export + SNTMetricInt64Gauge *gauge = [metricSet int64GaugeWithName:@"/foo/bar" + fieldNames:@[] + helpText:@"Number of callbacks done"]; + __block int count = 0; + [metricSet registerCallback:^(void) { + count++; + [gauge set:count forFieldValues:@[]]; + }]; + + // ensure the callback is called. + [metricSet export]; + + XCTAssertEqual([gauge getGaugeValueForFieldValues:@[]], 1); +} + +- (void)testAddConstantBool { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + [metricSet addConstantBooleanWithName:@"/tautology" + helpText:@"The first rule of tautology club is the first rule" + value:YES]; + + NSDictionary *expected = @{ + @"/tautology" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantBool], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithBool:true] + } ] + } + } + }; + + XCTAssertEqualObjects([metricSet export][@"metrics"], expected); +} + +- (void)testAddConstantString { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + + [metricSet addConstantStringWithName:@"/build/label" + helpText:@"Build label for the binary" + value:@"20210806.0.1"]; + + NSDictionary *expected = @{ + @"/build/label" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantString], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : @"20210806.0.1" + } ] + } + } + }; + + XCTAssertEqualObjects([metricSet export][@"metrics"], expected); +} + +- (void)testAddConstantInt { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] init]; + [metricSet addConstantIntegerWithName:@"/deep/thought/answer" + helpText:@"Life, the universe, and everything" + value:42]; + + NSDictionary *expected = @{ + @"/deep/thought/answer" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantInt64], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithLongLong:42] + } ] + } + } + }; + + XCTAssertEqualObjects([metricSet export][@"metrics"], expected); +} + +- (void)testExportNSDictionary { + SNTMetricSet *metricSet = [[SNTMetricSet alloc] initWithHostname:@"testHost" + username:@"testUser"]; + + // Add constants + [metricSet addConstantStringWithName:@"/build/label" + helpText:@"Software version running." + value:@"20210809.0.1"]; + [metricSet addConstantBooleanWithName:@"/santa/using_endpoint_security_framework" + helpText:@"Is santad using the endpoint security framework." + value:TRUE]; + [metricSet addConstantIntegerWithName:@"/proc/birth_timestamp" + helpText:@"Start time of this LogDumper instance, in microseconds " + @"since epoch" + value:(long long)(0x12345668910)]; + // Add Metrics + SNTMetricCounter *c = [metricSet counterWithName:@"/santa/events" + fieldNames:@[ @"rule_type" ] + helpText:@"Count of events on the host"]; + + [c incrementForFieldValues:@[ @"binary" ]]; + [c incrementBy:2 forFieldValues:@[ @"certificate" ]]; + + SNTMetricInt64Gauge *g = [metricSet int64GaugeWithName:@"/santa/rules" + fieldNames:@[ @"rule_type" ] + helpText:@"Number of rules."]; + + [g set:1 forFieldValues:@[ @"binary" ]]; + [g set:3 forFieldValues:@[ @"certificate" ]]; + + // Add Metrics with callback + SNTMetricInt64Gauge *virtualMemoryGauge = + [metricSet int64GaugeWithName:@"/proc/memory/virtual_size" + fieldNames:@[] + helpText:@"The virtual memory size of this process."]; + + SNTMetricInt64Gauge *residentMemoryGauge = + [metricSet int64GaugeWithName:@"/proc/memory/resident_size" + fieldNames:@[] + helpText:@"The resident set siz of this process."]; + + [metricSet registerCallback:^(void) { + [virtualMemoryGauge set:987654321 forFieldValues:@[]]; + [residentMemoryGauge set:123456789 forFieldValues:@[]]; + }]; + + NSDictionary *expected = @{ + @"root_labels" : @{@"hostname" : @"testHost", @"username" : @"testUser"}, + @"metrics" : @{ + @"/build/label" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantString], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : @"20210809.0.1" + } ] + } + }, + @"/santa/events" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeCounter], + @"fields" : @{ + @"rule_type" : @[ + @{ + @"value" : @"binary", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:1], + }, + @{ + @"value" : @"certificate", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:2], + }, + ], + }, + }, + @"/santa/rules" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64], + @"fields" : @{ + @"rule_type" : @[ + @{ + @"value" : @"binary", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:1], + }, + @{ + @"value" : @"certificate", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:3], + } + ] + }, + }, + @"/santa/using_endpoint_security_framework" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantBool], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithBool:YES] + } ] + } + }, + @"/proc/birth_timestamp" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantInt64], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithLong:1250999830800] + } ] + }, + }, + @"/proc/memory/virtual_size" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:987654321] + } ] + } + }, + @"/proc/memory/resident_size" : @{ + @"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64], + @"fields" : @{ + @"" : @[ @{ + @"value" : @"", + @"created" : [NSDate date], + @"last_updated" : [NSDate date], + @"data" : [NSNumber numberWithInt:123456789] + } ] + }, + }, + } + }; + + XCTAssertEqualObjects([metricSet export], expected); +} + +@end