Skip to content
This repository has been archived by the owner on Feb 5, 2025. It is now read-only.

Commit

Permalink
Add Support for Formatting metrics for Monarch (#633)
Browse files Browse the repository at this point in the history
* Initial commit of a Format that converts SNTMetricSet dictionaries to a format consumable by Monarch tooling.

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
  • Loading branch information
pmarkowsky and russellhancox authored Oct 11, 2021
1 parent 9e3943e commit 16d0bd6
Show file tree
Hide file tree
Showing 12 changed files with 563 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ tulsigen-*
*.pem
*.p12
*.keychain
*.swp
2 changes: 1 addition & 1 deletion Source/common/SNTCommonEnums.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ typedef NS_ENUM(NSInteger, SNTEventLogType) {
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
SNTMetricFormatTypeUnknown,
SNTMetricFormatTypeRawJSON,
SNTMetricFormatTypeJSON,
SNTMetricFormatTypeMonarchJSON,
};

static const char *kKextPath = "/Library/Extensions/santa-driver.kext";
Expand Down
5 changes: 3 additions & 2 deletions Source/common/SNTConfigurator.m
Original file line number Diff line number Diff line change
Expand Up @@ -671,12 +671,13 @@ - (BOOL)exportMetrics {

- (SNTMetricFormatType)metricFormat {
NSString *normalized = [self.configState[kMetricFormat] lowercaseString];

normalized = [normalized stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

if ([normalized isEqualToString:@"rawjson"]) {
return SNTMetricFormatTypeRawJSON;
} else if ([normalized isEqualToString:@"json"]) {
return SNTMetricFormatTypeJSON;
} else if ([normalized isEqualToString:@"monarchjson"]) {
return SNTMetricFormatTypeMonarchJSON;
} else {
return SNTMetricFormatTypeUnknown;
}
Expand Down
2 changes: 2 additions & 0 deletions Source/santametricservice/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTXPCMetricServiceInterface",
"//Source/santametricservice/Formats:SNTMetricMonarchJSONFormat",
"//Source/santametricservice/Formats:SNTMetricRawJSONFormat",
"//Source/santametricservice/Writers:SNTMetricFileWriter",
"//Source/santametricservice/Writers:SNTMetricHTTPWriter",
Expand All @@ -28,6 +29,7 @@ objc_library(
santa_unit_test(
name = "SNTMetricServiceTest",
srcs = ["SNTMetricServiceTest.m"],
structured_resources = ["//Source/santametricservice/Formats:testdata"],
deps = [
":SNTMetricServiceLib",
"//Source/santametricservice/Formats:SNTMetricFormatTestHelper",
Expand Down
32 changes: 32 additions & 0 deletions Source/santametricservice/Formats/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ objc_library(
],
)

objc_library(
name = "SNTMetricMonarchJSONFormat",
srcs = [
"SNTMetricFormat.h",
"SNTMetricMonarchJSONFormat.h",
"SNTMetricMonarchJSONFormat.m",
],
deps = [
":SNTMetricFormat",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
],
)

santa_unit_test(
name = "SNTMetricRawJSONFormatTest",
srcs = [
Expand All @@ -45,9 +59,27 @@ santa_unit_test(
],
)

santa_unit_test(
name = "SNTMetricMonarchJSONFormatTest",
srcs = [
"SNTMetricMonarchJSONFormatTest.m",
],
structured_resources = [":testdata"],
deps = [
":SNTMetricFormatTestHelper",
":SNTMetricMonarchJSONFormat",
],
)

filegroup(
name = "testdata",
srcs = glob(["testdata/**"]),
)

test_suite(
name = "format_tests",
tests = [
":SNTMetricMonarchJSONFormatTest",
":SNTMetricRawJSONFormatTest",
],
)
20 changes: 20 additions & 0 deletions Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// 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 <Foundation/Foundation.h>

#import "Source/santametricservice/Formats/SNTMetricFormat.h"

@interface SNTMetricMonarchJSONFormat : NSObject <SNTMetricFormat>
@end
223 changes: 223 additions & 0 deletions Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/// 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 "Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.h"

#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"

const NSString *kMetricsCollection = @"metricsCollection";
const NSString *kMetricsDataSet = @"metricsDataSet";
const NSString *kMetricName = @"metricName";
const NSString *kStreamKind = @"streamKind";
const NSString *kValueType = @"valueType";
const NSString *kDescription = @"description";
const NSString *kData = @"data";
const NSString *kFieldDescriptor = @"fieldDescriptor";
const NSString *kBoolValue = @"boolValue";
const NSString *kInt64Value = @"int64Value";
const NSString *kInt64ValueType = @"INT64";
const NSString *kStringValue = @"stringValue";
const NSString *kStringValueType = @"STRING";
const NSString *kName = @"name";
const NSString *kStartTimestamp = @"startTimestamp";
const NSString *kEndTimestamp = @"endTimestamp";
const NSString *kRootLabels = @"rootLabels";
const NSString *kKey = @"key";

@implementation SNTMetricMonarchJSONFormat {
NSISO8601DateFormatter *_dateFormatter;
}

- (instancetype)init {
self = [super init];
if (self) {
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
_dateFormatter.formatOptions = NSISO8601DateFormatWithInternetDateTime;

if (@available(macOS 10.13, *)) {
_dateFormatter.formatOptions |= NSISO8601DateFormatWithFractionalSeconds;
}
}
return self;
}

- (void)encodeValueAndStreamKindFor:(NSString *)metricName
withMetric:(NSDictionary *)metric
into:(NSMutableDictionary *)monarchMetric {
if (!metric[@"type"]) {
LOGE(@"metric type not supposed to be nil for %@", metricName);
return;
}

NSNumber *type = metric[@"type"];
if (![type isKindOfClass:[NSNumber class]]) {
LOGE(@"%@ [@\"type\"] is not a number", metricName);
return;
}

switch ((SNTMetricType)[type intValue]) {
case SNTMetricTypeConstantBool: monarchMetric[kValueType] = kBoolValue; break;
case SNTMetricTypeConstantString: monarchMetric[kValueType] = kStringValue; break;
case SNTMetricTypeConstantInt64: monarchMetric[kValueType] = kInt64Value; break;
case SNTMetricTypeConstantDouble: monarchMetric[kValueType] = @"DOUBLE"; break;
case SNTMetricTypeGaugeBool:
monarchMetric[kStreamKind] = @"GAUGE";
monarchMetric[kValueType] = kBoolValue;
break;
case SNTMetricTypeGaugeString:
monarchMetric[kStreamKind] = @"GAUGE";
monarchMetric[kValueType] = kStringValue;
break;
case SNTMetricTypeGaugeInt64:
monarchMetric[kStreamKind] = @"GAUGE";
monarchMetric[kValueType] = kInt64Value;
break;
case SNTMetricTypeGaugeDouble:
monarchMetric[kStreamKind] = @"GAUGE";
monarchMetric[kValueType] = @"DOUBLE";
break;
case SNTMetricTypeCounter:
monarchMetric[kStreamKind] = @"CUMULATIVE";
monarchMetric[kValueType] = kInt64Value;
break;
default:
LOGE(@"encountered unknown SNTMetricType - %ld for %@", (SNTMetricType)metric[@"type"],
metricName);
break;
}
}

- (NSArray<NSDictionary *> *)encodeDataForMetric:(NSDictionary *)metric {
NSMutableArray<NSDictionary *> *monarchMetricData = [[NSMutableArray alloc] init];

for (NSString *fieldName in metric[@"fields"]) {
for (NSDictionary *entry in metric[@"fields"][fieldName]) {
NSMutableDictionary *monarchDataEntry = [[NSMutableDictionary alloc] init];

monarchDataEntry[@"field"] = @[ @{kName : fieldName, kStringValue : entry[@"value"]} ];

monarchDataEntry[kStartTimestamp] = [self->_dateFormatter stringFromDate:entry[@"created"]];
monarchDataEntry[kEndTimestamp] =
[self->_dateFormatter stringFromDate:entry[@"last_updated"]];

if (!metric[@"type"]) {
LOGE(@"metric type is nil");
continue;
}

NSNumber *type = metric[@"type"];

switch ((SNTMetricType)[type intValue]) {
case SNTMetricTypeConstantBool:
case SNTMetricTypeGaugeBool: monarchDataEntry[kBoolValue] = entry[@"data"]; break;
case SNTMetricTypeConstantInt64:
case SNTMetricTypeGaugeInt64:
case SNTMetricTypeCounter: monarchDataEntry[kInt64Value] = entry[@"data"]; break;
case SNTMetricTypeConstantDouble:
case SNTMetricTypeGaugeDouble: monarchDataEntry[@"doubleValue"] = entry[@"data"]; break;
case SNTMetricTypeConstantString:
case SNTMetricTypeGaugeString: monarchDataEntry[kStringValue] = entry[@"data"]; break;
default: LOGE(@"encountered unknown SNTMetricType %ld", [type longValue]); break;
}
[monarchMetricData addObject:monarchDataEntry];
}
}

return monarchMetricData;
}

/*
* Translates SNTMetricSet fields to monarch's expected format. In this implementation only string
* type fields are supported.
*/
- (NSArray<NSDictionary *> *)encodeFieldsFor:(NSDictionary *)metric {
NSMutableArray<NSDictionary *> *monarchFields = [[NSMutableArray alloc] init];

for (NSString *fieldName in metric[@"fields"]) {
if (![fieldName isEqualToString:@""]) {
[monarchFields addObject:@{kName : fieldName, @"fieldType" : kStringValueType}];
}
}
return monarchFields;
}

/**
* formatMetric translates the SNTMetricSet metric entries into those consumable by Monarch.
**/

- (NSDictionary *)formatMetric:(NSString *)name withMetric:(NSDictionary *)metric {
NSMutableDictionary *monarchMetric = [[NSMutableDictionary alloc] init];

monarchMetric[kMetricName] = name;

if (metric[kDescription]) {
monarchMetric[kDescription] = metric[kDescription];
}

NSArray<NSDictionary *> *fieldDescriptorEntries = [self encodeFieldsFor:metric];
if (fieldDescriptorEntries.count > 0) {
monarchMetric[kFieldDescriptor] = [self encodeFieldsFor:metric];
}

[self encodeValueAndStreamKindFor:name withMetric:metric into:monarchMetric];
monarchMetric[@"data"] = [self encodeDataForMetric:metric];

return monarchMetric;
}

/**
* Normalizes the metrics dictionary for exporting to JSON
**/
- (NSDictionary *)normalize:(NSDictionary *)metrics {
NSMutableArray<NSDictionary *> *monarchMetrics = [[NSMutableArray alloc] init];

for (NSString *metricName in metrics[@"metrics"]) {
[monarchMetrics addObject:[self formatMetric:metricName
withMetric:metrics[@"metrics"][metricName]]];
}

NSMutableArray<NSDictionary *> *rootLabels = [[NSMutableArray alloc] init];

for (NSString *keyName in metrics[@"root_labels"]) {
[rootLabels addObject:@{kKey : keyName, kStringValue : metrics[@"root_labels"][keyName]}];
}

return @{kMetricsCollection : @[ @{kMetricsDataSet : monarchMetrics, kRootLabels : rootLabels} ]};
}

/*
* Convert normalizes and converts the metrics dictionary to a JSON
* object consumable by parts of Google Monarch's tooling.
*
* @param metrics an NSDictionary exported by the SNTMetricSet
* @param error a pointer to an NSError to allow errors to bubble up.
*
* Returns an NSArray containing one entry of all metrics serialized to JSON or
* nil on error.
*/
- (NSArray<NSData *> *)convert:(NSDictionary *)metrics error:(NSError **)err {
NSDictionary *normalizedMetrics = [self normalize:metrics];

NSData *json = [NSJSONSerialization dataWithJSONObject:normalizedMetrics
options:NSJSONWritingPrettyPrinted
error:err];

if (json == nil || (err != nil && *err != nil)) {
return nil;
}

return @[ json ];
}
@end
50 changes: 50 additions & 0 deletions Source/santametricservice/Formats/SNTMetricMonarchJSONFormatTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#import <XCTest/XCTest.h>

#import "Source/santametricservice/Formats/SNTMetricFormatTestHelper.h"
#import "Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.h"

@interface SNTMetricMonarchJSONFormatTest : XCTestCase
@end

@implementation SNTMetricMonarchJSONFormatTest

- (void)testMetricsConversionToJSON {
NSDictionary *validMetricsDict = [SNTMetricFormatTestHelper createValidMetricsDictionary];
SNTMetricMonarchJSONFormat *formatter = [[SNTMetricMonarchJSONFormat alloc] init];
NSError *err = nil;
NSArray<NSData *> *output = [formatter convert:validMetricsDict error:&err];

XCTAssertEqual(1, output.count);
XCTAssertNotNil(output[0]);
XCTAssertNil(err);

NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:output[0]
options:NSJSONReadingAllowFragments
error:&err];
XCTAssertNotNil(jsonDict);

NSString *path = [[NSBundle bundleForClass:[self class]] resourcePath];
path = [path stringByAppendingPathComponent:@"testdata/json/monarch.json"];

NSData *goldenFileData = [NSData dataWithContentsOfFile:path];

XCTAssertNotNil(goldenFileData, @"unable to open / read golden file");

NSDictionary *expectedJSONDict =
[NSJSONSerialization JSONObjectWithData:goldenFileData
options:NSJSONReadingAllowFragments
error:&err];

XCTAssertNotNil(expectedJSONDict);
XCTAssertEqualObjects(expectedJSONDict, jsonDict, @"generated JSON does not match golden file.");
}

- (void)testPassingANilOrNullErrorDoesNotCrash {
SNTMetricMonarchJSONFormat *formatter = [[SNTMetricMonarchJSONFormat alloc] init];
NSDictionary *validMetricsDict = [SNTMetricFormatTestHelper createValidMetricsDictionary];

NSArray<NSData *> *output = [formatter convert:validMetricsDict error:nil];
output = [formatter convert:validMetricsDict error:NULL];
}

@end
Loading

0 comments on commit 16d0bd6

Please sign in to comment.