Skip to content

Commit

Permalink
Initial commit of an HTTP writer for SNTMetricSets.
Browse files Browse the repository at this point in the history
This PR adds support for shipping serialized SNTMetricSets to an HTTP server via POSTs.
  • Loading branch information
pmarkowsky committed Oct 1, 2021
1 parent 58cec58 commit 994721f
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 4 deletions.
3 changes: 2 additions & 1 deletion Source/santametricservice/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ objc_library(
"//Source/common:SNTXPCMetricServiceInterface",
"//Source/santametricservice/Formats:SNTMetricRawJSONFormat",
"//Source/santametricservice/Writers:SNTMetricFileWriter",
"//Source/santametricservice/Writers:SNTMetricHTTPWriter",
"@MOLCodesignChecker",
"@MOLXPCConnection",
],
Expand All @@ -39,7 +40,7 @@ test_suite(
tests = [
":SNTMetricServiceTest",
"//Source/santametricservice/Formats:SNTMetricRawJSONFormatTest",
"//Source/santametricservice/Writers:SNTMetricFileWriterTest",
"//Source/santametricservice/Writers:writer_tests",
],
)

Expand Down
2 changes: 1 addition & 1 deletion Source/santametricservice/Formats/SNTMetricRawJSONFormat.m
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ - (NSDictionary *)normalize:(NSDictionary *)metrics {
if (![NSJSONSerialization isValidJSONObject:normalizedMetrics]) {
if (err != nil) {
*err = [[NSError alloc]
initWithDomain:@"SNTMetricRawJSONFileWriter"
initWithDomain:@"com.google.santa.metricservice.formatters.rawjson"
code:EINVAL
userInfo:@{
NSLocalizedDescriptionKey : @"unable to convert metrics to JSON: invalid metrics"
Expand Down
5 changes: 4 additions & 1 deletion Source/santametricservice/SNTMetricService.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#import "SNTMetricService.h"
#import "Source/santametricservice/Formats/SNTMetricRawJSONFormat.h"
#import "Source/santametricservice/Writers/SNTMetricFileWriter.h"
#import "Source/santametricservice/Writers/SNTMetricHTTPWriter.h"

@interface SNTMetricService ()
@property MOLXPCConnection *notifierConnection;
Expand All @@ -38,7 +39,9 @@ - (instancetype)init {
self = [super init];
if (self) {
rawJSONFormatter = [[SNTMetricRawJSONFormat alloc] init];
metricWriters = @{@"file" : [[SNTMetricFileWriter alloc] init]};
metricWriters = @{@"file" : [[SNTMetricFileWriter alloc] init],
@"http": [[SNTMetricHTTPWriter alloc] init],
};

_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
}
Expand Down
61 changes: 61 additions & 0 deletions Source/santametricservice/SNTMetricServiceTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#import "Source/common/SNTMetricSet.h"

#import <OCMock/OCMock.h>
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>

#import "Source/santametricservice/Formats/SNTMetricFormatTestHelper.h"
#import "Source/santametricservice/SNTMetricService.h"
Expand All @@ -16,6 +17,9 @@ @interface SNTMetricServiceTest : XCTestCase
@property id mockConfigurator;
@property NSString *tempDir;
@property NSURL *jsonURL;
@property id mockSession;
@property id mockSessionDataTask;
@property id mockMOLAuthenticatingURLSession;
@end

@implementation SNTMetricServiceTest
Expand All @@ -39,6 +43,15 @@ - (void)setUp {

- (void)tearDown {
[self.mockConfigurator stopMocking];
if (self.mockSessionDataTask != nil) {
[self.mockSessionDataTask stopMocking];
}
if (self.mockSession != nil) {
[self.mockSession stopMocking];
}
if (self.mockMOLAuthenticatingURLSession != nil) {
[self.mockMOLAuthenticatingURLSession stopMocking];
}

// delete the temp dir
[[NSFileManager defaultManager] removeItemAtPath:self.tempDir error:NULL];
Expand Down Expand Up @@ -112,4 +125,52 @@ - (void)testWritingRawJSONFile {

XCTAssertEqualObjects(validMetricsDict, parsedJSONData, @"invalid JSON created");
}

- (void)testWritingJSON {
NSURL *url = [NSURL URLWithString:@"http://localhost:9444"];
OCMStub([self.mockConfigurator exportMetrics]).andReturn(YES);
OCMStub([self.mockConfigurator metricFormat]).andReturn(SNTMetricFormatTypeRawJSON);
OCMStub([self.mockConfigurator metricURL]).andReturn(url);

self.mockSession = [OCMockObject niceMockForClass:[NSURLSession class]];
self.mockSessionDataTask = [OCMockObject niceMockForClass:[NSURLSessionDataTask class]];
self.mockMOLAuthenticatingURLSession =
[OCMockObject niceMockForClass:[MOLAuthenticatingURLSession class]];

[[[self.mockMOLAuthenticatingURLSession stub] andReturn:self.mockMOLAuthenticatingURLSession] alloc];
[[[self.mockMOLAuthenticatingURLSession stub] andReturn:self.mockSession] session];

NSHTTPURLResponse *response =
[[NSHTTPURLResponse alloc] initWithURL:url
statusCode:200
HTTPVersion:@"HTTP/1.1"
headerFields:@{@"content-type" : @"application/json"}];

__block void (^passedBlock)(NSData *, NSURLResponse *, NSError *);

XCTestExpectation *responseCallback = [[XCTestExpectation alloc] initWithDescription:@"ensure writer passed JSON"];

void (^getCompletionHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
[invocation getArgument:&passedBlock atIndex:3];
};

void (^callCompletionHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
passedBlock(nil, response, nil);
[responseCallback fulfill];
};


// stub out session to call completion handler immediately.
[(NSURLSessionDataTask *)[[self.mockSessionDataTask stub] andDo:callCompletionHandler] resume];

// stub out NSURLSession to assign our completion handler and return our mock
[[[[self.mockSession stub] andDo:getCompletionHandler] andReturn:self.mockSessionDataTask]
dataTaskWithRequest:[OCMArg any]
completionHandler:[OCMArg any]];

SNTMetricService *service = [[SNTMetricService alloc] init];
[service exportForMonitoring:[SNTMetricFormatTestHelper createValidMetricsDictionary]];
[self waitForExpectations:@[responseCallback] timeout:10.0];
}
@end

33 changes: 32 additions & 1 deletion Source/santametricservice/Writers/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ objc_library(
srcs = [
"SNTMetricFileWriter.h",
"SNTMetricFileWriter.m",
"SNTMetricWriter.h",
],
deps = [
":SNTMetricWriter",
Expand All @@ -31,3 +30,35 @@ santa_unit_test(
":SNTMetricFileWriter",
],
)

objc_library(
name = "SNTMetricHTTPWriter",
srcs = [
"SNTMetricHTTPWriter.h",
"SNTMetricHTTPWriter.m",
],
deps = [
":SNTMetricWriter",
"//Source/common:SNTLogging",
"@MOLAuthenticatingURLSession",
],
)

santa_unit_test(
name = "SNTMetricHTTPWriterTest",
srcs = [
"SNTMetricHTTPWriterTest.m",
],
deps = [
":SNTMetricHTTPWriter",
"@OCMock",
],
)

test_suite(
name = "writer_tests",
tests = [
":SNTMetricFileWriterTest",
":SNTMetricHTTPWriterTest",
],
)
18 changes: 18 additions & 0 deletions Source/santametricservice/Writers/SNTMetricHTTPWriter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// 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/Writers/SNTMetricWriter.h"

@interface SNTMetricHTTPWriter : NSObject <SNTMetricWriter>
@end
97 changes: 97 additions & 0 deletions Source/santametricservice/Writers/SNTMetricHTTPWriter.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/// 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.
#include <dispatch/dispatch.h>
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>

#import "Source/santametricservice/Writers/SNTMetricHTTPWriter.h"

@implementation SNTMetricHTTPWriter {
@private
NSMutableURLRequest *_request;
MOLAuthenticatingURLSession *_authSession;
NSURLSession *_session;
}

- (instancetype)init {
self = [super init];
if (self) {
_request = [[NSMutableURLRequest alloc] init];
_request.HTTPMethod = @"POST";
_authSession = [[MOLAuthenticatingURLSession alloc] init];
}
return self;
}

/**
* Post serialzied metrics to the specified URL one object at a time.
**/
- (BOOL)write:(NSArray<NSData *> *)metrics toURL:(NSURL *)url error:(NSError **)error {
// open the file and write it.
__block NSError *_blockError = nil;

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];

_authSession.serverHostname = url.host;
_session = _authSession.session;

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[metrics enumerateObjectsUsingBlock:^(id value, NSUInteger index, BOOL *stop) {
request.HTTPBody = (NSData *)value;
[[_session dataTaskWithRequest:request
completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
NSError *_Nullable err) {
if (err != nil) {
_blockError = err;
*stop = YES;
}

if (response == nil) {
*stop = YES;
} else if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

// Check HTTP error codes.
if (httpResponse && httpResponse.statusCode != 200) {
_blockError = [[NSError alloc]
initWithDomain:@"com.google.santa.metricservice.writers.http"
code:httpResponse.statusCode
userInfo:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"received http status code %ld from %@",
httpResponse.statusCode, url]
}];

*stop = YES;
}
}
}] resume];
}];
dispatch_semaphore_signal(semaphore);
});

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

if (_blockError != nil) {
if (error != nil) {
*error = [_blockError copy];
}
return NO;
}

return YES;
}
@end

0 comments on commit 994721f

Please sign in to comment.