From 4a7b5a974263b596bdaa1e74943c36d586b93f51 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Tue, 8 Oct 2013 16:49:30 -0700 Subject: [PATCH 01/16] Added async, unique queue notfiication helper. --- .../librato-iOS Demo/LibratoDemoAppDelegate.m | 1 + .../LibratoDemoEventTracker.h | 1 + .../LibratoDemoEventTracker.m | 25 +++++++++++++++++++ librato-iOS/Librato.h | 4 +++ librato-iOS/Librato.m | 15 +++++++++++ 5 files changed, 46 insertions(+) diff --git a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m index 14e47c0..e880c5a 100644 --- a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m +++ b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m @@ -25,6 +25,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [eventTracker groupDictionaryExample]; [eventTracker groupContextExample]; [eventTracker gaugeMetricExample]; + [eventTracker notificationExample]; return YES; } diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h index 4f547d1..f250379 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h @@ -22,5 +22,6 @@ - (void)groupDictionaryExample; - (void)groupContextExample; - (void)gaugeMetricExample; +- (void)notificationExample; @end diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index 1203bee..ea96bf5 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -117,6 +117,31 @@ - (void)groupContextExample } +/* + Provide the name of a notification and that notification will come into the block's context when it's caught. + Contexts are executed asynchronously in a Librato-specific serial queue. + A subscription with block is used and returned so you're responsible for unsubscribing when appropriate! +*/ +- (void)notificationExample +{ + __weak Librato *weakDemo = LibratoDemoEventTracker.sharedInstance; + id subscription = [LibratoDemoEventTracker.sharedInstance listenForNotification:@"state.sleeping" context:^(NSNotification *notification) { + LibratoMetric *useName = [LibratoMetric metricNamed:notification.name valued:@100 options:nil]; + LibratoMetric *useInfo = [LibratoMetric metricNamed:notification.userInfo[@"name"] valued:notification.userInfo[@"value"] options:notification.userInfo]; + + [weakDemo submit:@[useName, useInfo]]; + }]; + + [NSNotificationCenter.defaultCenter postNotificationName:@"state.sleeping" object:nil userInfo:@{ + @"name": @"infoName", + @"value": @42 + }]; + + // Don't forget to remove your subscriptions when you're done lest they hang around and point to a nil object! + [NSNotificationCenter.defaultCenter removeObserver:subscription]; +} + + /* Creates a series of counter measurements and submits them as a gague metric */ diff --git a/librato-iOS/Librato.h b/librato-iOS/Librato.h index bc21aea..be25348 100644 --- a/librato-iOS/Librato.h +++ b/librato-iOS/Librato.h @@ -23,10 +23,12 @@ extern NSString *const LIBRATO_LOCALIZABLE; typedef void (^LibratoMetricContext)(Librato *l); +typedef void (^LibratoNotificationContext)(NSNotification *notification); @property (nonatomic, strong) LibratoClient *client; @property (nonatomic, strong) NSString *prefix; +@property (nonatomic, strong) dispatch_queue_t queue; + (NSDate *)minimumMeasureTime; @@ -44,8 +46,10 @@ typedef void (^LibratoMetricContext)(Librato *l); - (void)getMeasurements:(NSString *)named options:(NSDictionary *)options; - (void)updateMetricsNamed:(NSString *)name options:(NSDictionary *)options; - (void)updateMetrics:(NSDictionary *)metrics; + - (NSArray *)groupNamed:(NSString *)name valued:(NSDictionary *)values; - (NSArray *)groupNamed:(NSString *)name context:(LibratoMetricContext)context; +- (id)listenForNotification:(NSString *)named context:(LibratoNotificationContext)context; - (void)submit; - (void)submit:(id)metrics; @end diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index d4f9644..9c7ba8f 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -30,6 +30,7 @@ - (instancetype)initWithEmail:(NSString *)email token:(NSString *)apiKey prefix: if((self = [super init])) { self.prefix = prefix ?: @""; + self.queue = dispatch_queue_create("LibratoQueue", NULL); [self authenticateEmail:email APIKey:apiKey]; } @@ -141,6 +142,20 @@ - (NSArray *)groupNamed:(NSString *)name context:(LibratoMetricContext)context } +- (id)listenForNotification:(NSString *)named context:(LibratoNotificationContext)context +{ + // TODO: Investigate using NSOperationQueue subclass instead of GCD inside of block. + // https://developer.apple.com/library/ios/featuredarticles/Short_Practical_Guide_Blocks/index.html#//apple_ref/doc/uid/TP40009758-CH1-SW33 + id subscription = [NSNotificationCenter.defaultCenter addObserverForName:named object:nil queue:nil usingBlock:^(NSNotification *note) { + dispatch_async(self.queue, ^{ + context(note); + }); + }]; + + return subscription; +} + + #pragma mark - Submission - (void)submit { From 24e9edbc8dc03546fb8976239503a4c3ce3aab52 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Tue, 8 Oct 2013 17:23:42 -0700 Subject: [PATCH 02/16] Tap into custom UA. --- .../LibratoDemoEventTracker.h | 1 + .../LibratoDemoEventTracker.m | 13 ++++++++++ librato-iOS/Classes/LibratoClient.m | 6 ----- librato-iOS/Librato.h | 4 +++- librato-iOS/Librato.m | 24 ++++++++++++++----- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h index f250379..eee48d3 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h @@ -23,5 +23,6 @@ - (void)groupContextExample; - (void)gaugeMetricExample; - (void)notificationExample; +- (void)customUAExample; @end diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index ea96bf5..3ad5f94 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -164,6 +164,19 @@ - (void)gaugeMetricExample } +/* + You can add a custom string the User Agent sent with all of the Librato requests + WARNING: Setting a custom UA will reset your client's connection so do not do this arbitrarily +*/ +- (void)customUAExample +{ + Librato *l = LibratoDemoEventTracker.sharedInstance; + l.customUserAgent = @"Demo UA"; + + [l submit:@{@"ua.custom.instances": @1}]; +} + + #pragma mark - Helpers - (NSNumber *)randomNumber { diff --git a/librato-iOS/Classes/LibratoClient.m b/librato-iOS/Classes/LibratoClient.m index 11b4184..e9ba623 100644 --- a/librato-iOS/Classes/LibratoClient.m +++ b/librato-iOS/Classes/LibratoClient.m @@ -31,12 +31,6 @@ - (instancetype)init return self; } -// TODO: Implelement -- (NSString *)agentIdentifier -{ - return @""; -} - - (void)authenticateEmail:(NSString *)emailAddress APIKey:(NSString *)apiKey { diff --git a/librato-iOS/Librato.h b/librato-iOS/Librato.h index be25348..33ac97a 100644 --- a/librato-iOS/Librato.h +++ b/librato-iOS/Librato.h @@ -38,10 +38,12 @@ typedef void (^LibratoNotificationContext)(NSNotification *notification); - (void)authenticateEmail:(NSString *)emailAddress APIKey:(NSString *)apiKey; - (NSString *)APIEndpoint; - (void)setAPIEndpoint:(NSString *)APIEndpoint; +- (LibratoConnection *)connection; +- (NSString *)customUserAgent; +- (void)setCustomUserAgent:(NSString *)userAgent; - (NSString *)persistence; - (void)setPersistence:(NSString *)persistence; - (id)persister; -- (LibratoConnection *)connection; - (void)getMetric:(NSString *)name options:(NSDictionary *)options; - (void)getMeasurements:(NSString *)named options:(NSDictionary *)options; - (void)updateMetricsNamed:(NSString *)name options:(NSDictionary *)options; diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index 9c7ba8f..ebccfdd 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -70,6 +70,24 @@ - (void)setAPIEndpoint:(NSString *)APIEndpoint } +- (LibratoConnection *)connection +{ + return self.client.connection; +} + + +- (NSString *)customUserAgent +{ + return self.client.customUserAgent; +} + + +- (void)setCustomUserAgent:(NSString *)userAgent +{ + self.client.customUserAgent = userAgent; +} + + - (NSString *)persistence { return self.client.persistence; @@ -88,12 +106,6 @@ - (void)setPersistence:(NSString *)persistence } -- (LibratoConnection *)connection -{ - return self.client.connection; -} - - - (void)getMetric:(NSString *)name options:(NSDictionary *)options { [self.client getMetric:name options:options]; From 0e10150892820ab7185bbd7752a2ec564d0cc458 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Wed, 9 Oct 2013 10:54:09 -0700 Subject: [PATCH 03/16] Metric creation helpers --- Demo/librato-iOS Demo/LibratoDemoAppDelegate.m | 1 + Demo/librato-iOS Demo/LibratoDemoEventTracker.h | 1 + Demo/librato-iOS Demo/LibratoDemoEventTracker.m | 14 ++++++++++++++ librato-iOS/Metrics/LibratoMetric.h | 2 ++ librato-iOS/Metrics/LibratoMetric.m | 16 ++++++++++++++++ 5 files changed, 34 insertions(+) diff --git a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m index e880c5a..c913825 100644 --- a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m +++ b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m @@ -26,6 +26,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [eventTracker groupContextExample]; [eventTracker gaugeMetricExample]; [eventTracker notificationExample]; + [eventTracker metricCreationHelpersExample]; return YES; } diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h index eee48d3..8abdca4 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h @@ -24,5 +24,6 @@ - (void)gaugeMetricExample; - (void)notificationExample; - (void)customUAExample; +- (void)metricCreationHelpersExample; @end diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index 3ad5f94..610d258 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -177,6 +177,20 @@ - (void)customUAExample } +/* + Metrics can be created with increasing levels of specificity + There are helpers for simple name & value metrics all the way up to all arguments specified +*/ +- (void)metricCreationHelpersExample +{ + LibratoMetric *basic = [LibratoMetric metricNamed:@"basic" valued:@1]; + LibratoMetric *explicit = [LibratoMetric metricNamed:@"explicit" valued:@100 source:@"demo" measureTime:NSDate.date]; + LibratoMetric *custom = [LibratoMetric metricNamed:@"custom" valued:@50 options:@{@"source": @"demo"}]; + + [LibratoDemoEventTracker.sharedInstance submit:@[basic, explicit, custom]]; +} + + #pragma mark - Helpers - (NSNumber *)randomNumber { diff --git a/librato-iOS/Metrics/LibratoMetric.h b/librato-iOS/Metrics/LibratoMetric.h index 453f420..025aa68 100644 --- a/librato-iOS/Metrics/LibratoMetric.h +++ b/librato-iOS/Metrics/LibratoMetric.h @@ -21,7 +21,9 @@ extern NSString *const LibratoMetricValueKey; @property (nonatomic, strong) NSString *type; - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options; ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value; + (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options; ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value source:(NSString *)source measureTime:(NSDate *)date; - (NSDictionary *)JSON; - (NSString *)source; diff --git a/librato-iOS/Metrics/LibratoMetric.m b/librato-iOS/Metrics/LibratoMetric.m index ec982b3..5db55b9 100644 --- a/librato-iOS/Metrics/LibratoMetric.m +++ b/librato-iOS/Metrics/LibratoMetric.m @@ -17,12 +17,27 @@ @implementation LibratoMetric #pragma mark - Lifecycle ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value +{ + return [LibratoMetric.alloc initWithName:name valued:value options:nil]; +} + + + (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options { return [LibratoMetric.alloc initWithName:name valued:value options:options]; } ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value source:(NSString *)source measureTime:(NSDate *)date +{ + return [LibratoMetric.alloc initWithName:name valued:value options:@{ + LibratoMetricSourceKey: source, + LibratoMetricMeasureTimeKey: date + }]; +} + + - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options { if ((self = super.init)) @@ -30,6 +45,7 @@ - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:( self.data = (options ? options.mutableCopy : @{}.mutableCopy); self.name = name; self.value = value ?: @0; + self.measureTime = options[LibratoMetricMeasureTimeKey] ?: NSDate.date; self.source = options[LibratoMetricSourceKey]; self.type = @"counters"; } From 7433254602cdc3d3b6d9b755766a929b82d73805 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Wed, 9 Oct 2013 11:12:13 -0700 Subject: [PATCH 04/16] Removed logs from demo. --- Demo/librato-iOS Demo/LibratoDemoEventTracker.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index 3ad5f94..6b749c3 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -51,8 +51,6 @@ - (void)counterMetricExample LibratoMetric *simpleMetric = [LibratoMetric metricNamed:@"works" valued:self.randomNumber options:@{@"source": @"demo app"}]; simpleMetric.measureTime = [NSDate dateWithTimeIntervalSinceNow:-(3600 * 24)]; - NSLog(@"%@", simpleMetric); - [LibratoDemoEventTracker.sharedInstance submit:simpleMetric]; } @@ -65,9 +63,6 @@ - (void)multipleMetricSubmissionExample LibratoMetric *memoryMetric = [LibratoMetric metricNamed:@"memory.available" valued:self.randomNumber options:nil]; LibratoMetric *storageMetric = [LibratoMetric metricNamed:@"storage.available" valued:self.randomNumber options:nil]; - NSLog(@"%@", memoryMetric); - NSLog(@"%@", storageMetric); - [LibratoDemoEventTracker.sharedInstance submit:@[memoryMetric, storageMetric]]; } @@ -157,7 +152,6 @@ - (void)gaugeMetricExample LibratoMetric *metric8 = [LibratoMetric metricNamed:@"bagels" valued:@0 options:nil]; NSArray *bagels = @[metric1, metric2, metric3, metric4, metric5, metric6, metric7, metric8]; - NSLog(@"%@", bagels); LibratoGaugeMetric *bagelGuage = [LibratoGaugeMetric metricNamed:@"bagel_guage" measurements:bagels]; [LibratoDemoEventTracker.sharedInstance submit:bagelGuage]; From e418ff7c1dd824c55529d0588ae6677a5a4b7062 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Mon, 14 Oct 2013 15:35:34 -0700 Subject: [PATCH 05/16] Converted metric models to Mantle instances. --- librato-iOS.podspec | 1 + librato-iOS/Classes/LibratoQueue.m | 16 ++-- librato-iOS/Metrics/LibratoGaugeMetric.h | 6 +- librato-iOS/Metrics/LibratoGaugeMetric.m | 46 ++++++---- librato-iOS/Metrics/LibratoMetric.h | 21 +++-- librato-iOS/Metrics/LibratoMetric.m | 110 +++++++++++------------ 6 files changed, 104 insertions(+), 96 deletions(-) diff --git a/librato-iOS.podspec b/librato-iOS.podspec index 5219f3a..8887649 100644 --- a/librato-iOS.podspec +++ b/librato-iOS.podspec @@ -18,4 +18,5 @@ Pod::Spec.new do |s| s.requires_arc = true s.dependency 'AFNetworking', '~> 1.0' + s.dependency 'Mantle', '~> 1.3' end diff --git a/librato-iOS/Classes/LibratoQueue.m b/librato-iOS/Classes/LibratoQueue.m index 7632752..a847028 100644 --- a/librato-iOS/Classes/LibratoQueue.m +++ b/librato-iOS/Classes/LibratoQueue.m @@ -75,7 +75,7 @@ - (LibratoQueue *)add:(id)metrics [self.queued addEntriesFromDictionary:@{metric.type: NSMutableArray.array}]; } - [(NSMutableArray *)[self.queued objectForKey:metric.type] addObject:metric.JSON]; + [(NSMutableArray *)[self.queued objectForKey:metric.type] addObject:metric.JSONDictionary]; }]; [self submitCheck]; @@ -108,16 +108,12 @@ - (NSArray *)createMetricsFromDictionary:(NSDictionary *)data } -- (NSString *)separateTypeFromMetric:(LibratoMetric *)metric +// TODO: Unused? Remove? +- (NSString *)separateTypeFromMetric:(LibratoMetric *)metric __deprecated { // This is too responsible. Metric should take care of mutation and answering this question. - NSString *typeKey = @"type"; - NSString *name = metric.data[typeKey]; - if (name) - { - [metric.data removeObjectForKey:typeKey]; - } - else + NSString *name = metric.type; + if (name.length == 0) { name = @"gauges"; } @@ -188,7 +184,7 @@ - (NSUInteger)size { __block NSUInteger result = 0; [self.queued enumerateKeysAndObjectsUsingBlock:^(id key, id data, BOOL *stop) { - result += [data count]; + result += [(NSMutableArray *)data count]; }]; return result; diff --git a/librato-iOS/Metrics/LibratoGaugeMetric.h b/librato-iOS/Metrics/LibratoGaugeMetric.h index aa7732a..69e2271 100644 --- a/librato-iOS/Metrics/LibratoGaugeMetric.h +++ b/librato-iOS/Metrics/LibratoGaugeMetric.h @@ -12,7 +12,11 @@ extern NSString *const LibratoMetricMeasurementsKey; @interface LibratoGaugeMetric : LibratoMetric -@property (nonatomic, strong) NSArray *measurements; +@property (nonatomic, strong) NSNumber *count; +@property (nonatomic, strong) NSNumber *sum; +@property (nonatomic, strong) NSNumber *min; +@property (nonatomic, strong) NSNumber *max; +@property (nonatomic, strong) NSNumber *squares; + (instancetype)metricNamed:(NSString *)name measurements:(NSArray *)measurements; diff --git a/librato-iOS/Metrics/LibratoGaugeMetric.m b/librato-iOS/Metrics/LibratoGaugeMetric.m index 7a4e939..fdf4404 100644 --- a/librato-iOS/Metrics/LibratoGaugeMetric.m +++ b/librato-iOS/Metrics/LibratoGaugeMetric.m @@ -23,7 +23,7 @@ @implementation LibratoGaugeMetric + (instancetype)metricNamed:(NSString *)name measurements:(NSArray *)measurements { LibratoGaugeMetric *metric = [LibratoGaugeMetric.alloc initWithName:name valued:nil options:nil]; - metric.measurements = measurements; + [metric computeMeasurements: measurements]; return metric; } @@ -34,7 +34,6 @@ - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:( if ((self = [super initWithName:name valued:nil options:options])) { self.type = @"gauges"; - [self.data removeObjectForKey:LibratoMetricValueKey]; } return self; @@ -44,27 +43,36 @@ - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:( #pragma mark - Calculations - (void)calculateStatisticsFromMeasurements:(NSArray *)measurements { - self.data[countKey] = [measurements valueForKeyPath:@"@count.self"]; - self.data[sumKey] = [measurements valueForKeyPath:@"@sum.value"]; - self.data[maxKey] = [measurements valueForKeyPath:@"@max.value"]; - self.data[minKey] = [measurements valueForKeyPath:@"@min.value"]; - self.data[squaresKey] = [measurements valueForKeyPath:@"@sum.squared"]; + _count = [measurements valueForKeyPath:@"@count.self"]; + _sum = [measurements valueForKeyPath:@"@sum.value"]; + _max = [measurements valueForKeyPath:@"@max.value"]; + _min = [measurements valueForKeyPath:@"@min.value"]; + _squares = [measurements valueForKeyPath:@"@sum.squared"]; } -#pragma mark - Properties -- (void)setMeasurements:(NSArray *)measurements +#pragma mark - MTLJSONSerializing ++ (NSDictionary *)JSONKeyPathsByPropertyKey { - if (measurements) - { - // TODO: TO have to remove this is a logic flow fault. Clean up in parent. - [self.data removeObjectForKey:@"value"]; - [self calculateStatisticsFromMeasurements:measurements]; - } - else - { - [self.data removeObjectsForKeys:@[countKey, sumKey, maxKey, minKey, squaresKey]]; - } + return @{ + @"name": LibratoMetricNameKey, + @"measureTime": LibratoMetricMeasureTimeKey, + @"source": LibratoMetricSourceKey, + @"count": countKey, + @"sum": sumKey, + @"min": minKey, + @"max": maxKey, + @"squares": squaresKey, + @"type": NSNull.null, + LibratoMetricValueKey: NSNull.null + }; +} + + +#pragma mark - Helpers +- (void)computeMeasurements:(NSArray *)measurements +{ + [self calculateStatisticsFromMeasurements:measurements]; } diff --git a/librato-iOS/Metrics/LibratoMetric.h b/librato-iOS/Metrics/LibratoMetric.h index 025aa68..7b08be6 100644 --- a/librato-iOS/Metrics/LibratoMetric.h +++ b/librato-iOS/Metrics/LibratoMetric.h @@ -7,28 +7,27 @@ // #import +#import "MTLModel.h" +#import "MTLJSONAdapter.h" extern NSString *const LibratoMetricMeasureTimeKey; extern NSString *const LibratoMetricNameKey; extern NSString *const LibratoMetricSourceKey; extern NSString *const LibratoMetricValueKey; -@interface LibratoMetric : NSObject +@interface LibratoMetric : MTLModel -@property (nonatomic, strong) NSMutableDictionary *data; -@property (nonatomic, strong) NSString *name; -@property (nonatomic, strong) NSDate *measureTime; -@property (nonatomic, strong) NSString *type; +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSDate *measureTime; +@property (nonatomic, copy) NSString *type; +@property (nonatomic, copy) NSString *source; +@property (nonatomic, copy) NSNumber *value; - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options; +- (NSDictionary *)JSONDictionary; + + (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value; + (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options; + (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value source:(NSString *)source measureTime:(NSDate *)date; -- (NSDictionary *)JSON; -- (NSString *)source; -- (void)setSource:(NSString *)source; -- (NSNumber *)value; -- (void)setValue:(NSNumber *)value; - @end diff --git a/librato-iOS/Metrics/LibratoMetric.m b/librato-iOS/Metrics/LibratoMetric.m index 5db55b9..50f4697 100644 --- a/librato-iOS/Metrics/LibratoMetric.m +++ b/librato-iOS/Metrics/LibratoMetric.m @@ -8,6 +8,7 @@ #import "LibratoMetric.h" #import "NSString+SanitizedForMetric.h" +#import "MTLValueTransformer.h" NSString *const LibratoMetricMeasureTimeKey = @"measure_time"; NSString *const LibratoMetricNameKey = @"name"; @@ -42,91 +43,90 @@ - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:( { if ((self = super.init)) { - self.data = (options ? options.mutableCopy : @{}.mutableCopy); - self.name = name; - self.value = value ?: @0; - self.measureTime = options[LibratoMetricMeasureTimeKey] ?: NSDate.date; - self.source = options[LibratoMetricSourceKey]; - self.type = @"counters"; + _name = name; + _value = value ?: @0; + _measureTime = options[LibratoMetricMeasureTimeKey] ?: NSDate.date; + _source = options[LibratoMetricSourceKey] ?: NSNull.null; + _type = @"counters"; } - + return self; } -#pragma mark - Properties -- (NSString *)name -{ - return self.data[LibratoMetricNameKey] ?: nil; -} - - -- (void)setName:(NSString *)name -{ - NSAssert(name.length > 0, @"Measurements must be named"); - self.data[LibratoMetricNameKey] = name.sanitizedForMetric; -} - - -- (NSDate *)measureTime -{ - return self.data[LibratoMetricMeasureTimeKey] ?: nil; -} - - -- (void)setMeasureTime:(NSDate *)measureTime +#pragma mark - MTLJSONSerializing ++ (NSDictionary *)JSONKeyPathsByPropertyKey { - self.data[LibratoMetricMeasureTimeKey] = measureTime; + return @{ + @"name": LibratoMetricNameKey, + @"value": LibratoMetricValueKey, + @"measureTime": LibratoMetricMeasureTimeKey, + @"source": LibratoMetricSourceKey, + @"type": NSNull.null + }; } -- (NSString *)source ++ (NSValueTransformer *)measureTimeJSONTransformer { - return self.data[LibratoMetricSourceKey] ?: nil; + return [MTLValueTransformer reversibleTransformerWithForwardBlock:^id(NSNumber *epoch) { + return [NSDate dateWithTimeIntervalSince1970:epoch.integerValue]; + } reverseBlock:^id(NSDate *date) { + return @(floor(date.timeIntervalSince1970)); + }]; } -- (void)setSource:(NSString *)source ++ (NSValueTransformer *)nameJSONTransformer { - if (source.length) - { - self.data[LibratoMetricSourceKey] = source.sanitizedForMetric; - } - else - { - [self.data removeObjectForKey:LibratoMetricSourceKey]; - } + return [MTLValueTransformer reversibleTransformerWithForwardBlock:^id(NSString *name) { + NSAssert(name.length > 0, @"Measurements must be named"); + return name.sanitizedForMetric; + } reverseBlock:^id(NSString *name) { + return name.sanitizedForMetric; + }]; } -- (NSNumber *)value ++ (NSValueTransformer *)sourceJSONTransformer { - return self.data[LibratoMetricValueKey] ?: nil; + return [MTLValueTransformer reversibleTransformerWithForwardBlock:^id(NSString *source) { + return source.sanitizedForMetric; + } reverseBlock:^id(NSString *source) { + return (source.length ? source.sanitizedForMetric : nil); + }]; } -- (void)setValue:(NSNumber *)value ++ (NSValueTransformer *)valueJSONTransformer { - NSAssert([self isValidValue:value], @"Boolean is not a valid metric value"); - self.data[LibratoMetricValueKey] = value; + return [MTLValueTransformer reversibleTransformerWithForwardBlock:^id(NSNumber *value) { + NSAssert([self.class isValidValue:value], @"Boolean is not a valid metric value"); + return value; + } reverseBlock:^id(NSNumber *value) { + return value; + }]; } -#pragma mark - Exporting data -- (NSDictionary *)JSON +// TODO: Some magic key's value for JSONDictionaryFromModel: so I don't need this method +- (NSDictionary *)JSONDictionary { - NSMutableDictionary *json = self.data.mutableCopy; - if ([self.measureTime isKindOfClass:NSDate.class]) - { - json[LibratoMetricMeasureTimeKey] = @(floor(self.measureTime.timeIntervalSince1970)); - } - - return json; + NSArray *nonNullableKeys = @[@"source"]; + __block NSMutableDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:self].mutableCopy; + [nonNullableKeys enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL *stop) { + if ([jsonDict.allKeys containsObject:key] && (jsonDict[key] == NSNull.null || jsonDict[key] == nil)) + { + [jsonDict removeObjectForKey:key]; + } + }]; + + return jsonDict; } #pragma mark - Validation -- (BOOL)isValidValue:(NSNumber *)value ++ (BOOL)isValidValue:(NSNumber *)value { return (strcmp([value objCType], @encode(BOOL)) == 0) ? NO : YES; } From 704c245a1710ac6989d13d8b54d50d24206d8c53 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Wed, 16 Oct 2013 14:06:12 -0700 Subject: [PATCH 06/16] Added collections which can be archived when offline. --- .../project.pbxproj | 8 +- .../librato-iOS Demo/LibratoDemoAppDelegate.m | 1 + librato-iOS.xcodeproj/project.pbxproj | 6 + librato-iOS/Classes/LibratoClient.h | 5 +- librato-iOS/Classes/LibratoClient.m | 121 +++++++++++++++++- librato-iOS/Classes/LibratoDirectPersister.m | 8 +- librato-iOS/Classes/LibratoProcessor.h | 3 +- librato-iOS/Classes/LibratoQueue.h | 4 + librato-iOS/Classes/LibratoQueue.m | 64 ++++++--- librato-iOS/Librato-Localizable.strings | 3 +- librato-iOS/Librato.h | 1 - librato-iOS/Librato.m | 8 +- librato-iOS/Metrics/LibratoMetricCollection.h | 22 ++++ librato-iOS/Metrics/LibratoMetricCollection.m | 65 ++++++++++ 14 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 librato-iOS/Metrics/LibratoMetricCollection.h create mode 100644 librato-iOS/Metrics/LibratoMetricCollection.m diff --git a/Demo/librato-iOS Demo.xcodeproj/project.pbxproj b/Demo/librato-iOS Demo.xcodeproj/project.pbxproj index 4cb791c..32b3347 100644 --- a/Demo/librato-iOS Demo.xcodeproj/project.pbxproj +++ b/Demo/librato-iOS Demo.xcodeproj/project.pbxproj @@ -334,7 +334,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -363,7 +363,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -374,7 +374,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -397,7 +397,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m index c913825..4eb4957 100644 --- a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m +++ b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m @@ -27,6 +27,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [eventTracker gaugeMetricExample]; [eventTracker notificationExample]; [eventTracker metricCreationHelpersExample]; + [eventTracker customUAExample]; return YES; } diff --git a/librato-iOS.xcodeproj/project.pbxproj b/librato-iOS.xcodeproj/project.pbxproj index 340430d..7396bdc 100644 --- a/librato-iOS.xcodeproj/project.pbxproj +++ b/librato-iOS.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 663C7E5B180E0FD70009065F /* LibratoMetricCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = 663C7E5A180E0FD70009065F /* LibratoMetricCollection.m */; }; 6692257E17FE44B800237E77 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6692257D17FE44B800237E77 /* Foundation.framework */; }; 6692258017FE44B800237E77 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6692257F17FE44B800237E77 /* CoreGraphics.framework */; }; 6692258217FE44B800237E77 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6692258117FE44B800237E77 /* UIKit.framework */; }; @@ -42,6 +43,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 663C7E59180E0FD70009065F /* LibratoMetricCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LibratoMetricCollection.h; sourceTree = ""; }; + 663C7E5A180E0FD70009065F /* LibratoMetricCollection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LibratoMetricCollection.m; sourceTree = ""; }; 6692257A17FE44B800237E77 /* librato-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "librato-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 6692257D17FE44B800237E77 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6692257F17FE44B800237E77 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -215,6 +218,8 @@ 669225C817FE458800237E77 /* LibratoGaugeMetric.m */, 669225C917FE458800237E77 /* LibratoMetric.h */, 669225CA17FE458800237E77 /* LibratoMetric.m */, + 663C7E59180E0FD70009065F /* LibratoMetricCollection.h */, + 663C7E5A180E0FD70009065F /* LibratoMetricCollection.m */, ); path = Metrics; sourceTree = ""; @@ -336,6 +341,7 @@ 669225C017FE458100237E77 /* LibratoClient.m in Sources */, 669225C117FE458100237E77 /* LibratoConnection.m in Sources */, 669225C317FE458100237E77 /* LibratoPersister.m in Sources */, + 663C7E5B180E0FD70009065F /* LibratoMetricCollection.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/librato-iOS/Classes/LibratoClient.h b/librato-iOS/Classes/LibratoClient.h index 07e112e..ac97d40 100644 --- a/librato-iOS/Classes/LibratoClient.h +++ b/librato-iOS/Classes/LibratoClient.h @@ -17,9 +17,11 @@ typedef void (^ClientFailureBlock)(NSError *error, NSDictionary *JSON); @interface LibratoClient : AFHTTPClient -@property (nonatomic, strong) NSString *APIEndpoint; @property (nonatomic, strong) NSString *agentIdentifier; +@property (nonatomic, strong) NSString *APIEndpoint; +@property (nonatomic, strong) NSString *archivePath; @property (nonatomic, strong) LibratoConnection *connection; +@property (nonatomic, assign, getter = isOnline) BOOL online; @property (nonatomic, strong) NSString *persistence; @property (nonatomic, strong) id persister; @property (nonatomic, strong) LibratoQueue *queue; @@ -27,6 +29,7 @@ typedef void (^ClientFailureBlock)(NSError *error, NSDictionary *JSON); - (void)authenticateEmail:(NSString *)emailAddress APIKey:(NSString *)apiKey; - (void)getMetric:(NSString *)name options:(NSDictionary *)options; - (void)getMeasurements:(NSString *)named options:(NSDictionary *)options; +- (NSDictionary *)metrics; - (void)sendPayload:(NSDictionary *)payload withSuccess:(ClientSuccessBlock)success orFailure:(ClientFailureBlock)failure; - (void)submit:(id)metrics; - (void)updateMetricsNamed:(NSString *)name options:(NSDictionary *)options; diff --git a/librato-iOS/Classes/LibratoClient.m b/librato-iOS/Classes/LibratoClient.m index e9ba623..0ef0fea 100644 --- a/librato-iOS/Classes/LibratoClient.m +++ b/librato-iOS/Classes/LibratoClient.m @@ -11,6 +11,7 @@ #import "LibratoQueue.h" NSString *const DEFAULT_API_ENDPIONT = @"https://metrics-api.librato.com/v1"; +NSString *const ARCHIVE_FILENAME = @"librato.archive"; NSString *email; NSString *APIKey; @@ -22,16 +23,112 @@ @interface LibratoClient () @implementation LibratoClient +#pragma mark - Lifecycle - (instancetype)init { self = [self initWithBaseURL:[NSURL URLWithString:DEFAULT_API_ENDPIONT]]; + if (self == nil) { + return nil; + } + [self setDefaultHeader:@"Accept" value:@"application/json"]; self.parameterEncoding = AFJSONParameterEncoding; - + self.online = NO; + + __weak __block LibratoClient *weakself = self; + [self setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { + weakself.online = (status != AFNetworkReachabilityStatusNotReachable); + }]; + + [self addObserver:self forKeyPath:NSStringFromSelector(@selector(online)) options:NSKeyValueObservingOptionNew context:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(handleForegroundNotificaiton:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(handleForegroundNotificaiton:) + name:UIApplicationDidFinishLaunchingNotification + object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(handleBackgroundNotification:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + return self; } +- (void)dealloc +{ + [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(online))]; + [NSNotificationCenter.defaultCenter removeObserver:self]; +} + + +#pragma mark - KVO +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([object isKindOfClass:LibratoClient.class]) + { + if ([keyPath isEqualToString:NSStringFromSelector(@selector(online))]) + { + if ([object isOnline]) + { + [self submit:nil]; + } + } + } +} + + +#pragma mark - Archiving +- (void)handleForegroundNotificaiton:(NSNotification *)notificaiton +{ + NSDictionary *metrics = [self unarchiveMetrics]; + if (metrics) { + // This is because add: can't tell collections from normal dictionaires + // TODO: Update add: to take collection name & models when type is found + [metrics enumerateKeysAndObjectsUsingBlock:^(NSString *key, LibratoMetric *metric, BOOL *stop) { + [self submit:metric]; + }]; + } +} + + +- (void)handleBackgroundNotification:(NSNotification *)notification +{ + [self archiveMetrics]; +} + + +- (void)archiveMetrics +{ + if (self.queue.isEmpty) return; + + NSDictionary *archived = [self unarchiveMetrics]; + if (archived) { + [self.queue merge:archived]; + } + + [NSKeyedArchiver archiveRootObject:self.metrics toFile:self.archivePath.stringByExpandingTildeInPath]; + [self.queue clear]; +} + + +- (NSDictionary *)unarchiveMetrics +{ + NSString *fullPath = self.archivePath.stringByExpandingTildeInPath; + if (![NSFileManager.defaultManager fileExistsAtPath:fullPath]) return nil; + + NSDictionary *metrics = [NSKeyedUnarchiver unarchiveObjectWithFile:fullPath]; + [NSFileManager.defaultManager removeItemAtPath:fullPath error:nil]; + return metrics; +} + + +#pragma mark - Helpers - (void)authenticateEmail:(NSString *)emailAddress APIKey:(NSString *)apiKey { [self flushAuthentication]; @@ -130,6 +227,7 @@ - (void)sendPayload:(NSDictionary *)payload withSuccess:(ClientSuccessBlock)succ if (success) { success(JSON, response.statusCode); + [self.queue clear]; } } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { if (failure) { @@ -153,8 +251,7 @@ - (void)flushAuthentication - (NSDictionary *)metrics { - // TODO: Implement - return NSDictionary.dictionary; + return self.queue.queued; } @@ -194,7 +291,11 @@ - (NSString *)persistence - (void)submit:(id)metrics { [self.queue add:metrics]; - [self.queue submit]; + + if (self.isOnline) + { + [self.queue submit]; + } } @@ -252,6 +353,17 @@ - (NSString *)APIEndpoint } +- (NSString *)archivePath +{ + if (!_archivePath) + { + _archivePath = [NSString stringWithFormat:@"%@/%@", NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0], ARCHIVE_FILENAME]; + } + + return _archivePath; +} + + - (LibratoConnection *)connection { if (!_connection) { @@ -297,5 +409,4 @@ - (NSString *)description } - @end diff --git a/librato-iOS/Classes/LibratoDirectPersister.m b/librato-iOS/Classes/LibratoDirectPersister.m index 0a3a2ab..ef8ef50 100644 --- a/librato-iOS/Classes/LibratoDirectPersister.m +++ b/librato-iOS/Classes/LibratoDirectPersister.m @@ -8,6 +8,7 @@ #import "LibratoClient.h" #import "LibratoDirectPersister.h" +#import "LibratoMetricCollection.h" @implementation LibratoDirectPersister @@ -21,7 +22,11 @@ - (BOOL)persistUsingClient:(LibratoClient *)client queued:(NSDictionary *)queued } else { - requests = @[queued]; + NSMutableDictionary *jsonRequests = @{}.mutableCopy; + [queued enumerateKeysAndObjectsUsingBlock:^(NSString *key, LibratoMetricCollection *collection, BOOL *stop) { + jsonRequests[key] = collection.toJSON; + }]; + requests = @[jsonRequests]; } [requests enumerateObjectsUsingBlock:^(NSDictionary *metricData, NSUInteger idx, BOOL *stop) { @@ -31,6 +36,7 @@ - (BOOL)persistUsingClient:(LibratoClient *)client queued:(NSDictionary *)queued // TODO: Hook for failure block }]; }]; + return YES; } diff --git a/librato-iOS/Classes/LibratoProcessor.h b/librato-iOS/Classes/LibratoProcessor.h index ecc6268..deb3b2e 100644 --- a/librato-iOS/Classes/LibratoProcessor.h +++ b/librato-iOS/Classes/LibratoProcessor.h @@ -8,12 +8,13 @@ #import #import "LibratoPersister.h" +#import "MTLModel.h" typedef void(^TimedExecutionBlock)(void); @class LibratoClient, LibratoMetric, LibratoPersister; -@interface LibratoProcessor : NSObject { +@interface LibratoProcessor : MTLModel { NSMutableDictionary *_queued; } diff --git a/librato-iOS/Classes/LibratoQueue.h b/librato-iOS/Classes/LibratoQueue.h index 6f54e8b..56c54bb 100644 --- a/librato-iOS/Classes/LibratoQueue.h +++ b/librato-iOS/Classes/LibratoQueue.h @@ -16,5 +16,9 @@ extern NSString *const QueueSkipMeasurementTimesKey; - (instancetype)initWithOptions:(NSDictionary *)options; - (LibratoQueue *)add:(id)metrics; +- (void)clear; +- (BOOL)isEmpty; +- (LibratoQueue *)merge:(NSDictionary *)dictionary; +- (NSUInteger)size; @end diff --git a/librato-iOS/Classes/LibratoQueue.m b/librato-iOS/Classes/LibratoQueue.m index a847028..2a579c8 100644 --- a/librato-iOS/Classes/LibratoQueue.m +++ b/librato-iOS/Classes/LibratoQueue.m @@ -7,6 +7,7 @@ // #import "Librato.h" +#import "LibratoMetricCollection.h" #import "LibratoQueue.h" #import "LibratoMetric.h" @@ -39,15 +40,20 @@ - (instancetype)initWithOptions:(NSDictionary *)options - (LibratoQueue *)add:(id)metrics { + if (!metrics) return self; + + // TODO: Clean up duplicate ways to add to the collection + if ([metrics isKindOfClass:LibratoMetric.class]) + { + [(NSMutableArray *)[self.queued objectForKey:((LibratoMetric *)metrics).type] addObject:metrics]; + return self; + } + NSArray *collection; if ([metrics isKindOfClass:NSArray.class]) { collection = metrics; } - else if ([metrics isKindOfClass:LibratoMetric.class]) - { - collection = @[metrics]; - } else if ([metrics isKindOfClass:NSDictionary.class]) { collection = [self createMetricsFromDictionary:metrics]; @@ -72,10 +78,10 @@ - (LibratoQueue *)add:(id)metrics // Sure, let's stack even more responsibility in this loop. What could possibly be bad about that? if (![self.queued.allKeys containsObject:metric.type]) { - [self.queued addEntriesFromDictionary:@{metric.type: NSMutableArray.array}]; + [self.queued addEntriesFromDictionary:@{metric.type: LibratoMetricCollection.new}]; } - - [(NSMutableArray *)[self.queued objectForKey:metric.type] addObject:metric.JSONDictionary]; + + [(LibratoMetricCollection *)[self.queued objectForKey:metric.type] addObject:metric]; }]; [self submitCheck]; @@ -96,12 +102,28 @@ - (NSArray *)createMetricsFromDictionary:(NSDictionary *)data { metric = [LibratoMetric metricNamed:key valued:value options:nil]; } + // TODO: Well this is certainly some grief + else if ([value respondsToSelector:@selector(enumerateObjectsUsingBlock:)]) + { + // Could be array of NSDictionary, LibratoMetric, whatev. Sanitize again. + [value enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [self add:obj]; + }]; + } + // TODO: Here's some more grief + else if ([value isKindOfClass:LibratoMetricCollection.class]) + { + [metrics addObjectsFromArray:((LibratoMetricCollection *)value).models]; + } else { metric = [LibratoMetric metricNamed:key valued:(NSNumber *)((NSDictionary *)value[@"value"]) options:value]; } - - [metrics addObject:metric]; + + if (metric) + { + [metrics addObject:metric]; + } }]; return metrics; @@ -136,7 +158,7 @@ - (BOOL)isEmpty - (void)clear { - self.queued = NSMutableDictionary.dictionary; + [self.queued removeAllObjects]; } @@ -146,15 +168,25 @@ - (void)flush } -- (NSArray *)gauges +- (NSArray *)gauges __deprecated { return self.queued[@"gauges"] ?: NSArray.array; } -- (LibratoQueue *)merge +- (LibratoQueue *)merge:(NSDictionary *)dictionary { - + [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, LibratoMetricCollection *collection, BOOL *stop) { + if (self.queued[key]) + { + [((LibratoMetricCollection *)self.queued[key]).models addObjectsFromArray:collection.models]; + } + else + { + self.queued[key] = collection; + } + }]; + return self; } @@ -183,8 +215,8 @@ - (NSMutableDictionary *)queued - (NSUInteger)size { __block NSUInteger result = 0; - [self.queued enumerateKeysAndObjectsUsingBlock:^(id key, id data, BOOL *stop) { - result += [(NSMutableArray *)data count]; + [self.queued enumerateKeysAndObjectsUsingBlock:^(id key, LibratoMetricCollection *collection, BOOL *stop) { + result += collection.models.count; }]; return result; @@ -245,7 +277,7 @@ - (void)submitCheck - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p, queued: %i>", NSStringFromClass([self class]), self, self.queued.count]; + return [NSString stringWithFormat:@"<%@: %p, queued: %i>", NSStringFromClass([self class]), self, self.size]; } diff --git a/librato-iOS/Librato-Localizable.strings b/librato-iOS/Librato-Localizable.strings index 7845299..5f89432 100644 --- a/librato-iOS/Librato-Localizable.strings +++ b/librato-iOS/Librato-Localizable.strings @@ -4,4 +4,5 @@ EXCEPTION_REASON_INVALID_DATA = "Invalid data"; EXCEPTION_REASON_INVALID_DATA_DATE_OUT_OF_BOUNDS_TEMPLATE = "Metric's date of %@ is invalid."; EXCEPTION_REASON_INVALID_DATA_MISSING_CREDENTIALS = "Missing email or API key"; EXCEPTION_REASON_INVALID_DATA_MISSING_START_OR_COUNT = "You must provide at least a startTime or count"; -EXCEPTION_REASON_INVALID_DATA_MISSING_SUCCESS_BLOCK = "You must provide a success block"; \ No newline at end of file +EXCEPTION_REASON_INVALID_DATA_MISSING_SUCCESS_BLOCK = "You must provide a success block"; +EXCEPTION_REASON_INVALID_DATA_MUST_BE_METRIC = "Collections must only contain LibratoMetric objects"; \ No newline at end of file diff --git a/librato-iOS/Librato.h b/librato-iOS/Librato.h index 33ac97a..ac2fa3b 100644 --- a/librato-iOS/Librato.h +++ b/librato-iOS/Librato.h @@ -52,6 +52,5 @@ typedef void (^LibratoNotificationContext)(NSNotification *notification); - (NSArray *)groupNamed:(NSString *)name valued:(NSDictionary *)values; - (NSArray *)groupNamed:(NSString *)name context:(LibratoMetricContext)context; - (id)listenForNotification:(NSString *)named context:(LibratoNotificationContext)context; -- (void)submit; - (void)submit:(id)metrics; @end diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index ebccfdd..564bae6 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -150,7 +150,7 @@ - (NSArray *)groupNamed:(NSString *)name context:(LibratoMetricContext)context self.client.queue.prefix = (originalPrefix.length ? [NSString stringWithFormat:@"%@.%@", originalPrefix, name] : name); context(self); self.client.queue.prefix = originalPrefix; - [self submit]; + [self submit:nil]; } @@ -169,12 +169,6 @@ - (id)listenForNotification:(NSString *)named context:(LibratoNotificationContex #pragma mark - Submission -- (void)submit -{ - [self.client.queue submit]; -} - - - (void)submit:(id)metrics { [self.client submit:metrics]; diff --git a/librato-iOS/Metrics/LibratoMetricCollection.h b/librato-iOS/Metrics/LibratoMetricCollection.h new file mode 100644 index 0000000..f67187d --- /dev/null +++ b/librato-iOS/Metrics/LibratoMetricCollection.h @@ -0,0 +1,22 @@ +// +// LibratoMetricCollection.h +// librato-iOS +// +// Created by Adam Yanalunas on 10/15/13. +// Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. +// + +#import +#import "MTLModel.h" + +@interface LibratoMetricCollection : MTLModel + +@property (nonatomic, strong) NSMutableArray *models; +@property (nonatomic, copy) NSString *name; + ++ (instancetype)collectionNamed:(NSString *)name; + +- (void)addObject:(id)object; +- (NSMutableArray *)toJSON; + +@end diff --git a/librato-iOS/Metrics/LibratoMetricCollection.m b/librato-iOS/Metrics/LibratoMetricCollection.m new file mode 100644 index 0000000..dbd9a76 --- /dev/null +++ b/librato-iOS/Metrics/LibratoMetricCollection.m @@ -0,0 +1,65 @@ +// +// LibratoMetricCollection.m +// librato-iOS +// +// Created by Adam Yanalunas on 10/15/13. +// Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. +// + +#import "LibratoMetricCollection.h" + +@implementation LibratoMetricCollection + + +#pragma mark - Lifecycle ++ (instancetype)collectionNamed:(NSString *)name +{ + LibratoMetricCollection *collection = [LibratoMetricCollection.alloc init]; + collection.name = name; + + return collection; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + + self.models = NSMutableArray.array; + + return self; +} + + +#pragma mark - Pseudo overrides +- (void)addObject:(id)object +{ + if (![object isKindOfClass:LibratoMetric.class]) + { + @throw [LibratoException exceptionWithReason:NSLocalizedString(@"EXCEPTION_REASON_INVALID_DATA_MUST_BE_METRIC", nil)]; + } + + [self.models addObject:object]; +} + + +#pragma mark - Helpers +- (NSMutableArray *)toJSON +{ + NSMutableArray *jsonModels = NSMutableArray.array; + [self.models enumerateObjectsUsingBlock:^(LibratoMetric *metric, NSUInteger idx, BOOL *stop) { + [jsonModels addObject:metric.JSONDictionary]; + }]; + + return jsonModels; +} + + +#pragma mark - Overrides +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, entries: %i>", NSStringFromClass([self class]), self, self.models.count]; +} + + +@end From 53fbe0bee6a22e34b698f212d01a188ea40b9468 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Wed, 16 Oct 2013 15:51:25 -0700 Subject: [PATCH 07/16] Fixed max age & metric missing right bucket. --- .../LibratoDemoEventTracker.m | 2 +- librato-iOS/Classes/LibratoQueue.m | 28 ++++++++++++++----- librato-iOS/Librato.m | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index 9dfbe9a..ccc3be5 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -49,7 +49,7 @@ + (Librato *)sharedInstance - (void)counterMetricExample { LibratoMetric *simpleMetric = [LibratoMetric metricNamed:@"works" valued:self.randomNumber options:@{@"source": @"demo app"}]; - simpleMetric.measureTime = [NSDate dateWithTimeIntervalSinceNow:-(3600 * 24)]; + simpleMetric.measureTime = [NSDate dateWithTimeIntervalSinceNow:-(60*5)]; [LibratoDemoEventTracker.sharedInstance submit:simpleMetric]; } diff --git a/librato-iOS/Classes/LibratoQueue.m b/librato-iOS/Classes/LibratoQueue.m index 2a579c8..33d96cc 100644 --- a/librato-iOS/Classes/LibratoQueue.m +++ b/librato-iOS/Classes/LibratoQueue.m @@ -45,7 +45,14 @@ - (LibratoQueue *)add:(id)metrics // TODO: Clean up duplicate ways to add to the collection if ([metrics isKindOfClass:LibratoMetric.class]) { - [(NSMutableArray *)[self.queued objectForKey:((LibratoMetric *)metrics).type] addObject:metrics]; + LibratoMetric *metric = metrics; + if (metric.measureTime) + { + [self checkMeasurementTime:metric]; + } + + LibratoMetricCollection *bucket = [self collectionNamed:metric.type]; + [bucket addObject:metrics]; return self; } @@ -76,12 +83,8 @@ - (LibratoQueue *)add:(id)metrics } // Sure, let's stack even more responsibility in this loop. What could possibly be bad about that? - if (![self.queued.allKeys containsObject:metric.type]) - { - [self.queued addEntriesFromDictionary:@{metric.type: LibratoMetricCollection.new}]; - } - - [(LibratoMetricCollection *)[self.queued objectForKey:metric.type] addObject:metric]; + LibratoMetricCollection *bucket = [self collectionNamed:metric.type]; + [bucket addObject:metric]; }]; [self submitCheck]; @@ -90,6 +93,17 @@ - (LibratoQueue *)add:(id)metrics } +- (LibratoMetricCollection *)collectionNamed:(NSString *)name +{ + if (![self.queued.allKeys containsObject:name]) + { + [self.queued addEntriesFromDictionary:@{name: [LibratoMetricCollection collectionNamed:name]}]; + } + + return self.queued[name]; +} + + - (NSArray *)createMetricsFromDictionary:(NSDictionary *)data { NSMutableArray *metrics = NSMutableArray.array; diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index 564bae6..1e9cebf 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -20,7 +20,7 @@ @implementation Librato #pragma mark - Class methods + (NSDate *)minimumMeasureTime { - return [NSDate.date dateByAddingTimeInterval:-(3600*24*365)]; + return [NSDate.date dateByAddingTimeInterval:-(60*15)]; } From ca7f20604fa9ab3dc40d6f52e3a735bc027efaa1 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Wed, 16 Oct 2013 16:29:20 -0700 Subject: [PATCH 08/16] Updated README with offline/cache info. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 10dc63f..db6b424 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ librato-iOS `librato-iOS` integrates with your iOS application (via [CocoaPods](http://cocoapods.org/)) to make reporting your metrics to [Librato](http://librato.com/) super easy. Reporting is done asynchronously and is designed to stay out of your way while allowing you to dig into each report's details, if you want. +Metrics are automatically cached while the network is unavailable and saved if the app closes before they're submitted. Don't worry about submitting metrics, we make sure they don't go missing before they can be handed off to Librato's service. + Currently iOS versions 6 and 7 are supported and the wonderful [AFNetworking](https://github.com/AFNetworking/AFNetworking) is used to handle network duties. # Quick Start @@ -19,6 +21,7 @@ Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" apiKey:@"a // You can provide an NSDictionary with values for the optional "source" and "measure_time" fields LibratoMetric *filesOpened = [LibratoMetric metricNamed:@"files.opened" valued:@42 options:nil]; // Optional values can be set directly on the metric object as well. +// NOTE: The maximum age for any submitted metric is fifteen minutes, as dictated by Librato. filesOpened.measureTime = [NSDate.date dateByAddingTimeInterval:-10]; [librato submit:filesOpened]; @@ -102,6 +105,15 @@ The `LibratoGroupMetric` automatically generates the count, sum, minimum, maximu There's an optional but highly-recommended prefix you can set which will automatically be added to all metric names. This is a great way to isolate data or quickly filter metrics. +# Offline Metric Gathering + +If the device loses network availability all new metrics are cached until the network (WiFi or cell) becomes again available. + +While offline every metric is stored in app memory so if memory consumption is a concern you may want to configure your app to reduce the amount of metrics gathered or turn off measurements after a certain amount have been gathered. Metrics themselves are very small so this should only be a concern if you're collecting many metrics per minute and will be offline for a lengthy period. + +# Persisting Metrics + +If the app caches metrics while offline and is then closed all cached metrics are stored in an `NSKeyedArchiver`. This archive is emptied into the queue the next time the app is opened. An `NSKeyedArchiver` is great for this purpose but it does not allow for any kind of querying of the data which means all archived metrics are blindly submitted, regardless of type or data. # Contribution From 1ef848c29c7c8161f5cf301d65ef6b207f695ebc Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Mon, 21 Oct 2013 11:39:29 -0700 Subject: [PATCH 09/16] Reviewer feedback, round 1 --- librato-iOS/Metrics/LibratoMetricCollection.h | 2 +- librato-iOS/Metrics/LibratoMetricCollection.m | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/librato-iOS/Metrics/LibratoMetricCollection.h b/librato-iOS/Metrics/LibratoMetricCollection.h index f67187d..7171088 100644 --- a/librato-iOS/Metrics/LibratoMetricCollection.h +++ b/librato-iOS/Metrics/LibratoMetricCollection.h @@ -16,7 +16,7 @@ + (instancetype)collectionNamed:(NSString *)name; -- (void)addObject:(id)object; +- (void)addObject:(LibratoMetric *)metric; - (NSMutableArray *)toJSON; @end diff --git a/librato-iOS/Metrics/LibratoMetricCollection.m b/librato-iOS/Metrics/LibratoMetricCollection.m index dbd9a76..45a910e 100644 --- a/librato-iOS/Metrics/LibratoMetricCollection.m +++ b/librato-iOS/Metrics/LibratoMetricCollection.m @@ -32,14 +32,9 @@ - (instancetype)init #pragma mark - Pseudo overrides -- (void)addObject:(id)object +- (void)addObject:(LibratoMetric *)metric { - if (![object isKindOfClass:LibratoMetric.class]) - { - @throw [LibratoException exceptionWithReason:NSLocalizedString(@"EXCEPTION_REASON_INVALID_DATA_MUST_BE_METRIC", nil)]; - } - - [self.models addObject:object]; + [self.models addObject:metric]; } From e3e095cb26579446400e9ac61a33fb9e940ef8da Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Mon, 21 Oct 2013 14:37:21 -0700 Subject: [PATCH 10/16] Exposed setters for submission success/fail blocks. --- .../librato-iOS Demo/LibratoDemoAppDelegate.m | 1 + .../LibratoDemoEventTracker.h | 1 + .../LibratoDemoEventTracker.m | 18 +++++++++++++ README.md | 26 +++++++++++++++++++ librato-iOS/Classes/LibratoClient.h | 3 +++ librato-iOS/Classes/LibratoClient.m | 6 +++++ librato-iOS/Classes/LibratoDirectPersister.m | 6 +---- librato-iOS/Librato.h | 4 ++- librato-iOS/Librato.m | 12 +++++++++ 9 files changed, 71 insertions(+), 6 deletions(-) diff --git a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m index 4eb4957..ef8fec9 100644 --- a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m +++ b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m @@ -28,6 +28,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [eventTracker notificationExample]; [eventTracker metricCreationHelpersExample]; [eventTracker customUAExample]; + [eventTracker submissionBlocksExample]; return YES; } diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h index 8abdca4..9f2a10b 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h @@ -25,5 +25,6 @@ - (void)notificationExample; - (void)customUAExample; - (void)metricCreationHelpersExample; +- (void)submissionBlocksExample; @end diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index ccc3be5..fce3265 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -185,6 +185,24 @@ - (void)metricCreationHelpersExample } +- (void)submissionBlocksExample +{ + Librato *libratoInstance = LibratoDemoEventTracker.sharedInstance; + + [libratoInstance setSubmitSuccessBlock:^(NSDictionary *JSON, NSUInteger code) { + if (code == 200) { + NSLog(@"Successful submission. Response JSON is: %@", JSON); + } + }]; + + [libratoInstance setSubmitFailureBlock:^(NSError *error, NSDictionary *JSON) { + NSLog(@"Error submitting metric: %@", error); + }]; + + [libratoInstance submit:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; +} + + #pragma mark - Helpers - (NSNumber *)randomNumber { diff --git a/README.md b/README.md index db6b424..d43d2a4 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,32 @@ The `LibratoGroupMetric` automatically generates the count, sum, minimum, maximu There's an optional but highly-recommended prefix you can set which will automatically be added to all metric names. This is a great way to isolate data or quickly filter metrics. +# Monitoring Submission Success or Failure + +You can set a blocks to handle the success and failure cases for metric submission. These are referenced when the submission calls back so sporadically setting or `nil`-ling the blocks may lead to unexpected results. + +```objective-c +Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" apiKey:@"abc123..." prefix:@""]; +[libratoInstance setSubmitSuccessBlock:^(NSDictionary *JSON, NSUInteger code) { + if (code == 200) { + NSLog(@"Successful submission. Response JSON is: %@", JSON); + } +}]; + +[libratoInstance setSubmitFailureBlock:^(NSError *error, NSDictionary *JSON) { + NSLog(@"Error submitting metric: %@", error); +}]; + +[libratoInstance submit:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; +``` + +If you want to disable the blocks, simply set them to `nil`. + +```objective-c +[libratoInstance setSubmitSuccessBlock:nil]; +[libratoInstance setSubmitFailureBlock:nil]; +``` + # Offline Metric Gathering If the device loses network availability all new metrics are cached until the network (WiFi or cell) becomes again available. diff --git a/librato-iOS/Classes/LibratoClient.h b/librato-iOS/Classes/LibratoClient.h index ac97d40..b6eba0a 100644 --- a/librato-iOS/Classes/LibratoClient.h +++ b/librato-iOS/Classes/LibratoClient.h @@ -25,11 +25,14 @@ typedef void (^ClientFailureBlock)(NSError *error, NSDictionary *JSON); @property (nonatomic, strong) NSString *persistence; @property (nonatomic, strong) id persister; @property (nonatomic, strong) LibratoQueue *queue; +@property (nonatomic, copy) ClientSuccessBlock submitSuccessBlock; +@property (nonatomic, copy) ClientFailureBlock submitFailureBlock; - (void)authenticateEmail:(NSString *)emailAddress APIKey:(NSString *)apiKey; - (void)getMetric:(NSString *)name options:(NSDictionary *)options; - (void)getMeasurements:(NSString *)named options:(NSDictionary *)options; - (NSDictionary *)metrics; +- (void)sendPayload:(NSDictionary *)payload; - (void)sendPayload:(NSDictionary *)payload withSuccess:(ClientSuccessBlock)success orFailure:(ClientFailureBlock)failure; - (void)submit:(id)metrics; - (void)updateMetricsNamed:(NSString *)name options:(NSDictionary *)options; diff --git a/librato-iOS/Classes/LibratoClient.m b/librato-iOS/Classes/LibratoClient.m index 0ef0fea..4c9d9cd 100644 --- a/librato-iOS/Classes/LibratoClient.m +++ b/librato-iOS/Classes/LibratoClient.m @@ -219,6 +219,12 @@ - (void)setUser:(NSString *)user andToken:(NSString *)token } +- (void)sendPayload:(NSDictionary *)payload +{ + [self sendPayload:payload withSuccess:self.submitSuccessBlock orFailure:self.submitFailureBlock]; +} + + - (void)sendPayload:(NSDictionary *)payload withSuccess:(ClientSuccessBlock)success orFailure:(ClientFailureBlock)failure { [self setUser:email andToken:APIKey]; diff --git a/librato-iOS/Classes/LibratoDirectPersister.m b/librato-iOS/Classes/LibratoDirectPersister.m index ef8ef50..3736ffb 100644 --- a/librato-iOS/Classes/LibratoDirectPersister.m +++ b/librato-iOS/Classes/LibratoDirectPersister.m @@ -30,11 +30,7 @@ - (BOOL)persistUsingClient:(LibratoClient *)client queued:(NSDictionary *)queued } [requests enumerateObjectsUsingBlock:^(NSDictionary *metricData, NSUInteger idx, BOOL *stop) { - [client sendPayload:metricData withSuccess:^(NSDictionary *JSON, NSUInteger code) { - // TODO: Hook for success block - } orFailure:^(NSError *error, NSDictionary *JSON) { - // TODO: Hook for failure block - }]; + [client sendPayload:metricData]; }]; return YES; diff --git a/librato-iOS/Librato.h b/librato-iOS/Librato.h index ac2fa3b..83d27f0 100644 --- a/librato-iOS/Librato.h +++ b/librato-iOS/Librato.h @@ -11,6 +11,7 @@ #import "LibratoGaugeMetric.h" #import "LibratoMetric.h" #import "LibratoPersister.h" +#import "LibratoClient.h" extern NSString *const LIBRATO_LOCALIZABLE; @@ -48,7 +49,8 @@ typedef void (^LibratoNotificationContext)(NSNotification *notification); - (void)getMeasurements:(NSString *)named options:(NSDictionary *)options; - (void)updateMetricsNamed:(NSString *)name options:(NSDictionary *)options; - (void)updateMetrics:(NSDictionary *)metrics; - +- (void)setSubmitSuccessBlock:(ClientSuccessBlock)successBlock; +- (void)setSubmitFailureBlock:(ClientFailureBlock)failureBlock; - (NSArray *)groupNamed:(NSString *)name valued:(NSDictionary *)values; - (NSArray *)groupNamed:(NSString *)name context:(LibratoMetricContext)context; - (id)listenForNotification:(NSString *)named context:(LibratoNotificationContext)context; diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index 1e9cebf..531ec46 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -130,6 +130,18 @@ - (void)updateMetrics:(NSDictionary *)metrics } +- (void)setSubmitSuccessBlock:(ClientSuccessBlock)successBlock +{ + self.client.submitSuccessBlock = successBlock; +} + + +- (void)setSubmitFailureBlock:(ClientFailureBlock)failureBlock +{ + self.client.submitFailureBlock = failureBlock; +} + + #pragma mark - Helpers - (NSArray *)groupNamed:(NSString *)name valued:(NSDictionary *)values { From ebc4dcd5ed976607f1e13acff5cdaa9fdcf26adb Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Mon, 21 Oct 2013 15:02:34 -0700 Subject: [PATCH 11/16] Assert that only custom init be used in Librato. --- librato-iOS/Librato.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index 531ec46..af57cb2 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -25,6 +25,15 @@ + (NSDate *)minimumMeasureTime #pragma mark - Lifecycle +- (instancetype)init +{ + NSAssert(false, @"You must use initWithEmail:token:prefix: to initialize a Librato instance"); + self = nil; + + return nil; +} + + - (instancetype)initWithEmail:(NSString *)email token:(NSString *)apiKey prefix:(NSString *)prefix { if((self = [super init])) From 5ce4d5d16b49dd5a09e21c5e09eb48881157c0d4 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Tue, 22 Oct 2013 15:44:46 -0700 Subject: [PATCH 12/16] Added auto collection of app/environment metrics. --- .../librato-iOS Demo-Info.plist | 2 +- librato-iOS/Classes/LibratoClient.m | 3 +- librato-iOS/Librato.h | 2 +- librato-iOS/Librato.m | 86 ++++++++++++++++++- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/Demo/librato-iOS Demo/librato-iOS Demo-Info.plist b/Demo/librato-iOS Demo/librato-iOS Demo-Info.plist index aa4894b..b09a861 100644 --- a/Demo/librato-iOS Demo/librato-iOS Demo-Info.plist +++ b/Demo/librato-iOS Demo/librato-iOS Demo-Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/librato-iOS/Classes/LibratoClient.m b/librato-iOS/Classes/LibratoClient.m index 4c9d9cd..33ceeaf 100644 --- a/librato-iOS/Classes/LibratoClient.m +++ b/librato-iOS/Classes/LibratoClient.m @@ -229,11 +229,12 @@ - (void)sendPayload:(NSDictionary *)payload withSuccess:(ClientSuccessBlock)succ { [self setUser:email andToken:APIKey]; NSURLRequest *request = [self requestWithMethod:@"POST" path:@"metrics" parameters:payload]; + // TODO: Move the queue into a local var that can be resotred if the submit fails + [self.queue clear]; AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { if (success) { success(JSON, response.statusCode); - [self.queue clear]; } } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { if (failure) { diff --git a/librato-iOS/Librato.h b/librato-iOS/Librato.h index 83d27f0..914fbf0 100644 --- a/librato-iOS/Librato.h +++ b/librato-iOS/Librato.h @@ -23,7 +23,7 @@ extern NSString *const LIBRATO_LOCALIZABLE; @interface Librato : NSObject -typedef void (^LibratoMetricContext)(Librato *l); +typedef void (^LibratoMetricContext)(Librato *librato); typedef void (^LibratoNotificationContext)(NSNotification *notification); diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index 531ec46..ba12266 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -12,9 +12,18 @@ #import "LibratoPersister.h" #import "LibratoQueue.h" #import "LibratoDirectPersister.h" +#import "LibratoVersion.h" NSString *const LIBRATO_LOCALIZABLE = @"Librato-Localizable"; + +@interface Librato () + +- (NSDictionary *)semanticVersionParts:(NSString *)versionString; + +@end + + @implementation Librato #pragma mark - Class methods @@ -32,6 +41,7 @@ - (instancetype)initWithEmail:(NSString *)email token:(NSString *)apiKey prefix: self.prefix = prefix ?: @""; self.queue = dispatch_queue_create("LibratoQueue", NULL); [self authenticateEmail:email APIKey:apiKey]; + [self trackDefaultMetrics]; } return self; @@ -162,7 +172,6 @@ - (NSArray *)groupNamed:(NSString *)name context:(LibratoMetricContext)context self.client.queue.prefix = (originalPrefix.length ? [NSString stringWithFormat:@"%@.%@", originalPrefix, name] : name); context(self); self.client.queue.prefix = originalPrefix; - [self submit:nil]; } @@ -180,6 +189,81 @@ - (id)listenForNotification:(NSString *)named context:(LibratoNotificationContex } +- (NSDictionary *)semanticVersionParts:(NSString *)versionString +{ + __block NSArray *versionParts = [versionString componentsSeparatedByString:@"."]; + __block NSMutableDictionary *versionLevels = @{}.mutableCopy; + + if (versionParts.count) { + [@[@"major", @"minor", @"patch"] enumerateObjectsUsingBlock:^(NSString *level, NSUInteger idx, BOOL *stop) { + if (versionParts.count > idx) { + NSNumber *num = @( ((NSString*)versionParts[idx]).integerValue ); + versionLevels[level] = num; + } + }]; + } + + return versionLevels; +} + + +#pragma mark - Default metric tracking +- (void)trackDefaultMetrics +{ + [self trackDeviceMetrics]; + [self trackOSMetrics]; + [self trackAppMetrics]; + [self trackLibraryMetrics]; +} + + +- (void)trackDeviceMetrics +{ + UIScreen *mainScreen = UIScreen.mainScreen; + CGSize screen = mainScreen.bounds.size; + LibratoMetric *screenCount = [LibratoMetric metricNamed:@"device.screen.count" valued:@(UIScreen.screens.count)]; + LibratoMetric *screenScale = [LibratoMetric metricNamed:@"device.screen.scale" valued:@(mainScreen.scale)]; + LibratoMetric *screenWidth = [LibratoMetric metricNamed:@"device.screen.width" valued:@(screen.width)]; + LibratoMetric *screenHeight = [LibratoMetric metricNamed:@"device.screen.height" valued:@(screen.height)]; + + [self submit:@[screenScale, screenCount, screenWidth, screenHeight]]; +} + + +- (void)trackOSMetrics +{ + UIDevice *device = UIDevice.currentDevice; + NSMutableArray *versionLevels = @[].mutableCopy; + NSDictionary *semanticVersionParts = [self semanticVersionParts:device.systemVersion]; + + [semanticVersionParts enumerateKeysAndObjectsUsingBlock:^(NSString *level, NSNumber *value, BOOL *stop) { + [versionLevels addObject:[LibratoMetric metricNamed:[NSString stringWithFormat:@"%@.%@", @"os.version", level] valued:value]]; + }]; + + [self submit:versionLevels]; +} + + +- (void)trackAppMetrics +{ + NSString *bundleString = [NSBundle.mainBundle.infoDictionary objectForKey:@"CFBundleShortVersionString"]; + NSMutableArray *versionLevels = @[].mutableCopy; + NSDictionary *semanticVersionParts = [self semanticVersionParts:bundleString]; + + [semanticVersionParts enumerateKeysAndObjectsUsingBlock:^(NSString *level, NSNumber *value, BOOL *stop) { + [versionLevels addObject:[LibratoMetric metricNamed:[NSString stringWithFormat:@"%@.%@", @"app", level] valued:value]]; + }]; + + [self submit:versionLevels]; +} + + +- (void)trackLibraryMetrics +{ + [self submit:[LibratoMetric metricNamed:@"librato-iOS.version" valued:@(LibratoVersion.version.floatValue)]]; +} + + #pragma mark - Submission - (void)submit:(id)metrics { From fda9cbaeaa4525e61bff0c53932d94b2a6c47190 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Tue, 22 Oct 2013 18:04:01 -0700 Subject: [PATCH 13/16] Added autosubmit timer. Prefers add:, not submit: --- .../LibratoDemoEventTracker.m | 28 +++++++++---------- README.md | 28 ++++++++++++++++--- librato-iOS/Classes/LibratoProcessor.h | 1 + librato-iOS/Classes/LibratoProcessor.m | 23 ++++++++++++++- librato-iOS/Librato.h | 1 + librato-iOS/Librato.m | 14 +++++++--- 6 files changed, 72 insertions(+), 23 deletions(-) diff --git a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m index fce3265..7a562a4 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -51,28 +51,28 @@ - (void)counterMetricExample LibratoMetric *simpleMetric = [LibratoMetric metricNamed:@"works" valued:self.randomNumber options:@{@"source": @"demo app"}]; simpleMetric.measureTime = [NSDate dateWithTimeIntervalSinceNow:-(60*5)]; - [LibratoDemoEventTracker.sharedInstance submit:simpleMetric]; + [LibratoDemoEventTracker.sharedInstance add:simpleMetric]; } /* - Creates two different metrics but submits them simultaneously + Creates two different metrics but adds them simultaneously */ - (void)multipleMetricSubmissionExample { LibratoMetric *memoryMetric = [LibratoMetric metricNamed:@"memory.available" valued:self.randomNumber options:nil]; LibratoMetric *storageMetric = [LibratoMetric metricNamed:@"storage.available" valued:self.randomNumber options:nil]; - [LibratoDemoEventTracker.sharedInstance submit:@[memoryMetric, storageMetric]]; + [LibratoDemoEventTracker.sharedInstance add:@[memoryMetric, storageMetric]]; } /* - Creates and auto-submits two counter metrics: "meaning" and "plutonium", the latter using an NSDictionary to set the value and source simultaneously + Creates two counter metrics: "meaning" and "plutonium", the latter using an NSDictionary to set the value and source simultaneously */ - (void)dictionaryCreationExample { - [LibratoDemoEventTracker.sharedInstance submit:@{@"meaning": self.randomNumber, @"plutonium": @{@"value": @238, @"source": @"Russia, with love"}}]; + [LibratoDemoEventTracker.sharedInstance add:@{@"meaning": self.randomNumber, @"plutonium": @{@"value": @238, @"source": @"Russia, with love"}}]; } @@ -84,7 +84,7 @@ - (void)dictionaryCreationExample The group prefix is the first argument and is joined to each metric named with a period. The dictionary's key value is the metric name as an NSString and the value is an NSNumber value. - If the group is named "foo" and the first metric is named "bar" it will be submitted with the name "foo.bar" + If the group is named "foo" and the first metric is named "bar" it metric's submitted name will be "foo.bar" */ - (void)groupDictionaryExample { @@ -94,7 +94,7 @@ - (void)groupDictionaryExample @"friends": @172 }; NSArray *metrics = [LibratoDemoEventTracker.sharedInstance groupNamed:@"user" valued:valueDict]; - [LibratoDemoEventTracker.sharedInstance submit:metrics]; + [LibratoDemoEventTracker.sharedInstance add:metrics]; } @@ -107,7 +107,7 @@ - (void)groupContextExample LibratoMetric *logins = [LibratoMetric metricNamed:@"logins" valued:@12 options:nil]; LibratoMetric *logouts = [LibratoMetric metricNamed:@"logouts" valued:@7 options:nil]; LibratoMetric *timeouts = [LibratoMetric metricNamed:@"timeouts" valued:@5 options:nil]; - [l submit:@[logins, logouts, timeouts]]; + [l add:@[logins, logouts, timeouts]]; }]; } @@ -124,7 +124,7 @@ - (void)notificationExample LibratoMetric *useName = [LibratoMetric metricNamed:notification.name valued:@100 options:nil]; LibratoMetric *useInfo = [LibratoMetric metricNamed:notification.userInfo[@"name"] valued:notification.userInfo[@"value"] options:notification.userInfo]; - [weakDemo submit:@[useName, useInfo]]; + [weakDemo add:@[useName, useInfo]]; }]; [NSNotificationCenter.defaultCenter postNotificationName:@"state.sleeping" object:nil userInfo:@{ @@ -138,7 +138,7 @@ - (void)notificationExample /* - Creates a series of counter measurements and submits them as a gague metric + Creates a series of counter measurements and adds them as a gague metric */ - (void)gaugeMetricExample { @@ -154,7 +154,7 @@ - (void)gaugeMetricExample NSArray *bagels = @[metric1, metric2, metric3, metric4, metric5, metric6, metric7, metric8]; LibratoGaugeMetric *bagelGuage = [LibratoGaugeMetric metricNamed:@"bagel_guage" measurements:bagels]; - [LibratoDemoEventTracker.sharedInstance submit:bagelGuage]; + [LibratoDemoEventTracker.sharedInstance add:bagelGuage]; } @@ -167,7 +167,7 @@ - (void)customUAExample Librato *l = LibratoDemoEventTracker.sharedInstance; l.customUserAgent = @"Demo UA"; - [l submit:@{@"ua.custom.instances": @1}]; + [l add:@{@"ua.custom.instances": @1}]; } @@ -181,7 +181,7 @@ - (void)metricCreationHelpersExample LibratoMetric *explicit = [LibratoMetric metricNamed:@"explicit" valued:@100 source:@"demo" measureTime:NSDate.date]; LibratoMetric *custom = [LibratoMetric metricNamed:@"custom" valued:@50 options:@{@"source": @"demo"}]; - [LibratoDemoEventTracker.sharedInstance submit:@[basic, explicit, custom]]; + [LibratoDemoEventTracker.sharedInstance add:@[basic, explicit, custom]]; } @@ -199,7 +199,7 @@ - (void)submissionBlocksExample NSLog(@"Error submitting metric: %@", error); }]; - [libratoInstance submit:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; + [libratoInstance add:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; } diff --git a/README.md b/README.md index d43d2a4..7543a8f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ LibratoMetric *filesOpened = [LibratoMetric metricNamed:@"files.opened" valued:@ // NOTE: The maximum age for any submitted metric is fifteen minutes, as dictated by Librato. filesOpened.measureTime = [NSDate.date dateByAddingTimeInterval:-10]; -[librato submit:filesOpened]; +[librato add:filesOpened]; ``` # Installation @@ -75,10 +75,10 @@ metric.source = @"the internet"; metric.measureTime = [NSDate.date dateByAddingTimeInterval:-(3600 * 24)] ``` -Optionally, you can create one or more counters inline with an NSDictionary when submitting. +Optionally, you can create one or more counters inline with an NSDictionary when adding. ```objective-c -[ submit:@{@"downloads": @13, @"plutonium": @{@"value": @238, @"source": @"Russia, with love"}}]; +[ add:@{@"downloads": @13, @"plutonium": @{@"value": @238, @"source": @"Russia, with love"}}]; ``` ### Grouping @@ -101,6 +101,26 @@ LibratoGaugeMetric *bagelGuage = [LibratoGaugeMetric metricNamed:@"bagel_guage" The `LibratoGroupMetric` automatically generates the count, sum, minimum, maximum and square values for the aggregate data for use in the reporting tool. +# Submitting + +It is usually unnecessary to manually submit metrics. By default, `librato-iOS` will automatically submit anything that has been added to the queue every five seconds, if interent connectivity is available. + +Use the `autosubmitInterval` option when initializing a `LibratoQueue` instance to configure how often submissions should be triggered. + +This interval can be adjusted to any `NSTimeInterval` measurement but `librato-iOS` will only run the check for your timer once every second to avoid automated flooding. + +### Manual submission + +If you have a metrics you'd like to add to the queue and trigger an immediate submission you can use the `submit:` method. This accepts either metrics or a `nil` value. + +```objective-c +// Adding metrics and immediately triggering a submission +[ submit:metrics]; + +// Passing nil will simply trigger the submission +[ submit:nil]; +``` + # Custom Prefix There's an optional but highly-recommended prefix you can set which will automatically be added to all metric names. This is a great way to isolate data or quickly filter metrics. @@ -121,7 +141,7 @@ Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" apiKey:@"a NSLog(@"Error submitting metric: %@", error); }]; -[libratoInstance submit:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; +[libratoInstance add:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; ``` If you want to disable the blocks, simply set them to `nil`. diff --git a/librato-iOS/Classes/LibratoProcessor.h b/librato-iOS/Classes/LibratoProcessor.h index deb3b2e..74fde3a 100644 --- a/librato-iOS/Classes/LibratoProcessor.h +++ b/librato-iOS/Classes/LibratoProcessor.h @@ -19,6 +19,7 @@ typedef void(^TimedExecutionBlock)(void); } @property (nonatomic) NSTimeInterval autosubmitInterval; +@property (nonatomic, strong) NSTimer *autoSubmitTimer; @property (nonatomic) BOOL clearOnFailure; @property (nonatomic, strong) NSDate *createTime; @property (nonatomic, strong) NSMutableDictionary *queued; diff --git a/librato-iOS/Classes/LibratoProcessor.m b/librato-iOS/Classes/LibratoProcessor.m index c317b2c..a309d5c 100644 --- a/librato-iOS/Classes/LibratoProcessor.m +++ b/librato-iOS/Classes/LibratoProcessor.m @@ -13,6 +13,8 @@ #import "LibratoProcessor.h" static NSUInteger MEASUREMENTS_PER_REQUEST = 500; +static NSTimeInterval MINIMUM_AUTOSUBMIT_INTERVAL = 1; +static NSTimeInterval SECONDS_BETWEEN_AUTOSUBMITS = 5; @interface LibratoProcessor () @@ -21,6 +23,17 @@ @interface LibratoProcessor () @implementation LibratoProcessor +#pragma mark - Lifecycle +- (void)dealloc +{ + if (self.autoSubmitTimer) + { + [self.autoSubmitTimer invalidate]; + } +} + + +#pragma mark - Submission - (BOOL)submit { if (self.queued.count == 0) @@ -76,7 +89,9 @@ - (NSTimeInterval)epochTime - (void)setupCommonOptions:(NSDictionary *)options { - self.autosubmitInterval = ((NSNumber *)options[@"autosubmitInterval"]).doubleValue; + self.autosubmitInterval = (options[@"autosubmitInterval"] ? ((NSNumber *)options[@"autosubmitInterval"]).doubleValue : SECONDS_BETWEEN_AUTOSUBMITS); + self.autoSubmitTimer = [NSTimer timerWithTimeInterval:MINIMUM_AUTOSUBMIT_INTERVAL target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES]; + [NSRunLoop.currentRunLoop addTimer:self.autoSubmitTimer forMode:NSDefaultRunLoopMode]; self.client = options[@"client"] ?: LibratoClient.new; _perRequest = options[@"perRequest"] ? ((NSNumber *)options[@"perRequest"]).integerValue : MEASUREMENTS_PER_REQUEST; self.source = options[@"source"]; @@ -88,6 +103,12 @@ - (void)setupCommonOptions:(NSDictionary *)options } +- (void)handleTimer:(NSTimer *)timer +{ + [self autosubmitCheck]; +} + + - (void)autosubmitCheck { if (self.autosubmitInterval) diff --git a/librato-iOS/Librato.h b/librato-iOS/Librato.h index 914fbf0..e17aed6 100644 --- a/librato-iOS/Librato.h +++ b/librato-iOS/Librato.h @@ -36,6 +36,7 @@ typedef void (^LibratoNotificationContext)(NSNotification *notification); - (instancetype)initWithEmail:(NSString *)email token:(NSString *)apiKey prefix:(NSString *)prefix; - (LibratoClient *)client; +- (void)add:(id)metrics; - (void)authenticateEmail:(NSString *)emailAddress APIKey:(NSString *)apiKey; - (NSString *)APIEndpoint; - (void)setAPIEndpoint:(NSString *)APIEndpoint; diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index af0f95b..4e96a62 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -235,7 +235,7 @@ - (void)trackDeviceMetrics LibratoMetric *screenWidth = [LibratoMetric metricNamed:@"device.screen.width" valued:@(screen.width)]; LibratoMetric *screenHeight = [LibratoMetric metricNamed:@"device.screen.height" valued:@(screen.height)]; - [self submit:@[screenScale, screenCount, screenWidth, screenHeight]]; + [self add:@[screenScale, screenCount, screenWidth, screenHeight]]; } @@ -249,7 +249,7 @@ - (void)trackOSMetrics [versionLevels addObject:[LibratoMetric metricNamed:[NSString stringWithFormat:@"%@.%@", @"os.version", level] valued:value]]; }]; - [self submit:versionLevels]; + [self add:versionLevels]; } @@ -263,17 +263,23 @@ - (void)trackAppMetrics [versionLevels addObject:[LibratoMetric metricNamed:[NSString stringWithFormat:@"%@.%@", @"app", level] valued:value]]; }]; - [self submit:versionLevels]; + [self add:versionLevels]; } - (void)trackLibraryMetrics { - [self submit:[LibratoMetric metricNamed:@"librato-iOS.version" valued:@(LibratoVersion.version.floatValue)]]; + [self add:[LibratoMetric metricNamed:@"librato-iOS.version" valued:@(LibratoVersion.version.floatValue)]]; } #pragma mark - Submission +- (void)add:(id)metrics +{ + [self.client.queue add:metrics]; +} + + - (void)submit:(id)metrics { [self.client submit:metrics]; From a21b350d37d1c761c999f6bd7acccd8c5264f795 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Thu, 24 Oct 2013 11:26:23 -0700 Subject: [PATCH 14/16] Locking down inits for metric creation. --- librato-iOS/Metrics/LibratoGaugeMetric.m | 9 +++++++++ librato-iOS/Metrics/LibratoMetric.m | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/librato-iOS/Metrics/LibratoGaugeMetric.m b/librato-iOS/Metrics/LibratoGaugeMetric.m index fdf4404..16ee6df 100644 --- a/librato-iOS/Metrics/LibratoGaugeMetric.m +++ b/librato-iOS/Metrics/LibratoGaugeMetric.m @@ -29,6 +29,15 @@ + (instancetype)metricNamed:(NSString *)name measurements:(NSArray *)measurement } +- (instancetype)init +{ + NSAssert(false, @"You must use initWithName:valued:options: to initialize a LibratoGagueMetric instance"); + self = nil; + + return nil; +} + + - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options { if ((self = [super initWithName:name valued:nil options:options])) diff --git a/librato-iOS/Metrics/LibratoMetric.m b/librato-iOS/Metrics/LibratoMetric.m index 50f4697..706aea5 100644 --- a/librato-iOS/Metrics/LibratoMetric.m +++ b/librato-iOS/Metrics/LibratoMetric.m @@ -39,6 +39,15 @@ + (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value source:(NS } +- (instancetype)init +{ + NSAssert(false, @"You must use initWithName:valued:options: to initialize a LibratoMetric instance"); + self = nil; + + return nil; +} + + - (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options { if ((self = super.init)) From 149a15c437f3be443f77d5e625577426307c9a0b Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Thu, 24 Oct 2013 11:27:03 -0700 Subject: [PATCH 15/16] Cleaned up property and method interfaces. --- librato-iOS/Classes/LibratoClient.h | 6 +++--- librato-iOS/Classes/LibratoConnection.h | 2 +- librato-iOS/Classes/LibratoConnection.m | 2 -- librato-iOS/Classes/LibratoProcessor.h | 7 ++++--- librato-iOS/Classes/LibratoProcessor.m | 2 +- librato-iOS/Classes/LibratoQueue.m | 6 +++--- librato-iOS/Metrics/LibratoMetricCollection.h | 3 ++- librato-iOS/Metrics/LibratoMetricCollection.m | 10 ++++++++-- 8 files changed, 22 insertions(+), 16 deletions(-) diff --git a/librato-iOS/Classes/LibratoClient.h b/librato-iOS/Classes/LibratoClient.h index b6eba0a..cbeb90e 100644 --- a/librato-iOS/Classes/LibratoClient.h +++ b/librato-iOS/Classes/LibratoClient.h @@ -17,9 +17,9 @@ typedef void (^ClientFailureBlock)(NSError *error, NSDictionary *JSON); @interface LibratoClient : AFHTTPClient -@property (nonatomic, strong) NSString *agentIdentifier; -@property (nonatomic, strong) NSString *APIEndpoint; -@property (nonatomic, strong) NSString *archivePath; +@property (nonatomic, copy) NSString *agentIdentifier; +@property (nonatomic, copy) NSString *APIEndpoint; +@property (nonatomic, copy) NSString *archivePath; @property (nonatomic, strong) LibratoConnection *connection; @property (nonatomic, assign, getter = isOnline) BOOL online; @property (nonatomic, strong) NSString *persistence; diff --git a/librato-iOS/Classes/LibratoConnection.h b/librato-iOS/Classes/LibratoConnection.h index a0d6a1b..1497854 100644 --- a/librato-iOS/Classes/LibratoConnection.h +++ b/librato-iOS/Classes/LibratoConnection.h @@ -12,7 +12,7 @@ @interface LibratoConnection : NSObject -@property (nonatomic, strong) NSString *APIEndpoint; +@property (nonatomic, copy) NSString *APIEndpoint; @property (nonatomic, strong) LibratoClient *client; - (instancetype)initWithClient:(LibratoClient *)client usingEndpoint:(NSString *)endpoint; diff --git a/librato-iOS/Classes/LibratoConnection.m b/librato-iOS/Classes/LibratoConnection.m index 15b53b4..24fa61e 100644 --- a/librato-iOS/Classes/LibratoConnection.m +++ b/librato-iOS/Classes/LibratoConnection.m @@ -6,11 +6,9 @@ // Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. // -#import #import "LibratoConnection.h" #import "LibratoClient.h" #import "LibratoVersion.h" -#import NSString *const DEFAULT_API_ENDPOINT = @"https://metrics-api.librato.com"; NSString *const DEFAULT_API_VERSION = @"v1"; diff --git a/librato-iOS/Classes/LibratoProcessor.h b/librato-iOS/Classes/LibratoProcessor.h index 74fde3a..b57bfee 100644 --- a/librato-iOS/Classes/LibratoProcessor.h +++ b/librato-iOS/Classes/LibratoProcessor.h @@ -23,18 +23,19 @@ typedef void(^TimedExecutionBlock)(void); @property (nonatomic) BOOL clearOnFailure; @property (nonatomic, strong) NSDate *createTime; @property (nonatomic, strong) NSMutableDictionary *queued; -@property (nonatomic, strong) NSString *source; +@property (nonatomic, copy) NSString *source; @property (nonatomic, strong) NSDate *measureTime; @property (nonatomic, strong) LibratoClient *client; @property (nonatomic, strong, readonly) NSDate *lastSubmitTime; @property (nonatomic, strong) id persister; @property (nonatomic, readonly) NSUInteger perRequest; -@property (nonatomic, strong) NSString *prefix; +@property (nonatomic, copy) NSString *prefix; + ++ (NSTimeInterval)epochTime; - (BOOL)submit; - (LibratoMetric *)time:(TimedExecutionBlock)block named:(NSString *)name options:(NSDictionary *)options; - (id)createPersister; -- (NSTimeInterval)epochTime; - (void)setupCommonOptions:(NSDictionary *)options; - (void)autosubmitCheck; diff --git a/librato-iOS/Classes/LibratoProcessor.m b/librato-iOS/Classes/LibratoProcessor.m index a309d5c..fcfe7c6 100644 --- a/librato-iOS/Classes/LibratoProcessor.m +++ b/librato-iOS/Classes/LibratoProcessor.m @@ -81,7 +81,7 @@ - (LibratoMetric *)time:(TimedExecutionBlock)block named:(NSString *)name option return NSClassFromString(type).new; } -- (NSTimeInterval)epochTime ++ (NSTimeInterval)epochTime { return [NSDate.date timeIntervalSince1970]; } diff --git a/librato-iOS/Classes/LibratoQueue.m b/librato-iOS/Classes/LibratoQueue.m index 33d96cc..d964443 100644 --- a/librato-iOS/Classes/LibratoQueue.m +++ b/librato-iOS/Classes/LibratoQueue.m @@ -52,7 +52,7 @@ - (LibratoQueue *)add:(id)metrics } LibratoMetricCollection *bucket = [self collectionNamed:metric.type]; - [bucket addObject:metrics]; + [bucket addMetric:metrics]; return self; } @@ -79,12 +79,12 @@ - (LibratoQueue *)add:(id)metrics else if (![self skipMeasurementTimes]) { // Should probably just default to epochTime when a Metric is created - metric.measureTime = [NSDate dateWithTimeIntervalSince1970:self.epochTime]; + metric.measureTime = [NSDate dateWithTimeIntervalSince1970:self.class.epochTime]; } // Sure, let's stack even more responsibility in this loop. What could possibly be bad about that? LibratoMetricCollection *bucket = [self collectionNamed:metric.type]; - [bucket addObject:metric]; + [bucket addMetric:metric]; }]; [self submitCheck]; diff --git a/librato-iOS/Metrics/LibratoMetricCollection.h b/librato-iOS/Metrics/LibratoMetricCollection.h index 7171088..cc1c0a7 100644 --- a/librato-iOS/Metrics/LibratoMetricCollection.h +++ b/librato-iOS/Metrics/LibratoMetricCollection.h @@ -16,7 +16,8 @@ + (instancetype)collectionNamed:(NSString *)name; -- (void)addObject:(LibratoMetric *)metric; +- (void)addMetric:(LibratoMetric *)metric; +- (void)removeMetric:(LibratoMetric *)metric; - (NSMutableArray *)toJSON; @end diff --git a/librato-iOS/Metrics/LibratoMetricCollection.m b/librato-iOS/Metrics/LibratoMetricCollection.m index 45a910e..7657fa7 100644 --- a/librato-iOS/Metrics/LibratoMetricCollection.m +++ b/librato-iOS/Metrics/LibratoMetricCollection.m @@ -31,13 +31,19 @@ - (instancetype)init } -#pragma mark - Pseudo overrides -- (void)addObject:(LibratoMetric *)metric +#pragma mark - Model management +- (void)addMetric:(LibratoMetric *)metric { [self.models addObject:metric]; } +- (void)removeMetric:(LibratoMetric *)metric +{ + [self.models removeObject:metric]; +} + + #pragma mark - Helpers - (NSMutableArray *)toJSON { From 4ffe39ca3de3a9ac05d870e9061df6da5ae9a877 Mon Sep 17 00:00:00 2001 From: Adam Yanalunas Date: Thu, 24 Oct 2013 11:36:33 -0700 Subject: [PATCH 16/16] Updated docs for 1.0.0 release --- CHANGELOG.md | 93 ++++++++++++++++++++++++++++++++++++++++++++- README.md | 62 ++++++++++++++++++------------ librato-iOS.podspec | 4 +- 3 files changed, 132 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 181dc4a..e91919a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,94 @@ +### Version 1.0.0 + +This is a major revision which means **APIs will break**. It is not backwards compatible with 0.1.x releases. Code from 0.1.x branches will no longer be supported. Please update! + +#### Demo Project + +* Added descriptions and examples on how to use various features to demo project + +#### Metrics + +* Added `metricNamed:valued:` to `LibratoMetric` (https://github.com/amco/librato-iOS/commit/0e10150892820ab7185bbd7752a2ec564d0cc458) +* Added `metricNamed:valued:source:measureTime:` to `LibratoMetric` (https://github.com/amco/librato-iOS/commit/0e10150892820ab7185bbd7752a2ec564d0cc458) +* Fixed `metricTime` not being set when passed in via `metricNamed:valued:options:` (https://github.com/amco/librato-iOS/commit/0e10150892820ab7185bbd7752a2ec564d0cc458) +* Changed metrics to extend Mantle instead of `NSObject` (https://github.com/amco/librato-iOS/commit/e418ff7c1dd824c55529d0588ae6677a5a4b7062) +* Changed `isValidValue` from instance to class method +* Changed maximum metric age from one year to fifteen minutes (Librato Metric rules) (https://github.com/amco/librato-iOS/commit/53fbe0bee6a22e34b698f212d01a188ea40b9468) +* Added automatic collection of device, OS, app and Librato library metrics when a `Librato` instance is initialized (https://github.com/amco/librato-iOS/commit/5ce4d5d16b49dd5a09e21c5e09eb48881157c0d4) +* Fixed `LibratoClient.metrics` to report queued metrics instead of blank `NSDictionary` +* Fixed queue firing `removeAllObjects` when `clear`ing instead of overwriting with new `NSMutableDictonary` so dictionary children are `release`d. (https://github.com/amco/librato-iOS/commit/704c245a1710ac6989d13d8b54d50d24206d8c53) + +#### Collections + +* Added `LibratoMetricCollection` which contains metrics based on type and handles conversion of metrics into structured JSON (https://github.com/amco/librato-iOS/commit/704c245a1710ac6989d13d8b54d50d24206d8c53) + +#### Initialization + +* Added `NSAsserts` in Librato, LibratoMetric and LibratoGaugeMetric `init` to disable use in favor of their custom initialization methods (https://github.com/amco/librato-iOS/commit/ebc4dcd5ed976607f1e13acff5cdaa9fdcf26adb) + +#### Submission + +* Added `add:` interface which is preferred over `submit:` +* Changed manual submission to an optional command as queues are automatically submitted on a configurable interval (https://github.com/amco/librato-iOS/commit/fda9cbaeaa4525e61bff0c53932d94b2a6c47190) +* Added global block handlers for submission success and failure (https://github.com/amco/librato-iOS/commit/e3e095cb26579446400e9ac61a33fb9e940ef8da) +* Changed queue to clear just before firing submission instead of after successful submission to prevent accidental double submission (https://github.com/amco/librato-iOS/commit/5ce4d5d16b49dd5a09e21c5e09eb48881157c0d4) +* Note: Queue is not cached before clearing, would could be useful if submission fails to re-queue items + +#### Offline + +* Added prevention of metrics submission if device is offline (https://github.com/amco/librato-iOS/commit/704c245a1710ac6989d13d8b54d50d24206d8c53) +* Added automatic queue submission when internet becomes available +* Added storage of queue in `NSKeyedArchiver` when app is backgrounded +* Added queue hydration via `NSKeyedArchiver` when app is brought to foreground + +#### Group metrics + +* Added `groupNamed:valued:` to convert an `NSDictionary` into an array of `LibratoMetric`s (https://github.com/amco/librato-iOS/commit/fa4a9a5cf525e6ed04192e41b8bb709e57612a57) +* Added `groupNamed:context:` to automatically prefix any metrics created in the context with the group name + +#### Notification subscription + +* Added ability of `Librato` to subscribe to notifications with `listenForNotification:context:` and perform given `context` when notification is caught (https://github.com/amco/librato-iOS/commit/4a7b5a974263b596bdaa1e74943c36d586b93f51) +* Added queue specific to Librato subscriptions for `dispatch_async`ing execution of assigned `context` + +#### User agent + +* Added custom user agent setting available in `Librato` (https://github.com/amco/librato-iOS/commit/24e9edbc8dc03546fb8976239503a4c3ce3aab52) +* Removed `agentIdentifier` from `LibratoClient` + +#### Descriptions + +* Added custom descriptions for Librato, LibratoClient, LibratoMetric, LibratoMetricCollection and LibratoQueue to aid debugging (https://github.com/amco/librato-iOS/commit/704c245a1710ac6989d13d8b54d50d24206d8c53) + +#### Miscellaneous + +* Removed numerous `NSLog`s. Sorry about the extra noise. (https://github.com/amco/librato-iOS/commit/474fe9a115ffe308eb2e858a93af0453568e76ad, https://github.com/amco/librato-iOS/commit/7433254602cdc3d3b6d9b755766a929b82d73805) + ### Version 0.1.0 -* Initial commit and functionality +Initial commit and functionality + +* Code available via CocoaPods + +#### Metrics + +* Create counter metric +* Create group metric, statistics automatically computed +* Name and source fields automatically cleaned and trimmed +* Custom prefix available to be applied to all metric names +* Values for all fields can be manipulated after initialization + +#### Submission + +* Metric types offered but `NSDictionary` data automatically parsed into appropriate Metric type and queued +* Metrics only queued until manual submission +* Only available parser is direct JSON parsing + +#### Queue + +* Add-only, no management +* Manual submission + +#### Localization + +* Error messages localized for English diff --git a/README.md b/README.md index 7543a8f..cfc62b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ librato-iOS =========== -`librato-iOS` integrates with your iOS application (via [CocoaPods](http://cocoapods.org/)) to make reporting your metrics to [Librato](http://librato.com/) super easy. Reporting is done asynchronously and is designed to stay out of your way while allowing you to dig into each report's details, if you want. +`librato-iOS` integrates with your iOS application (via [CocoaPods](http://cocoapods.org/)) to make reporting your metrics to [Librato](http://librato.com/) super easy. Reporting is done asynchronously and is designed to stay out of your way while allowing you to dig into each metric's details, if you want. Metrics are automatically cached while the network is unavailable and saved if the app closes before they're submitted. Don't worry about submitting metrics, we make sure they don't go missing before they can be handed off to Librato's service. @@ -14,16 +14,16 @@ After installing `librato-iOS` into your workspace with CocoaPods just create a ```objective-c #import "Librato.h" -... -// The prefix is optional but recommended as it helps you organize across your different projects -Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" apiKey:@"abc123..." prefix:@""]; +// The prefix is optional but recommended as it +// helps you organize across your different projects +Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" + apiKey:@"abc123..." + prefix:@""]; -// You can provide an NSDictionary with values for the optional "source" and "measure_time" fields -LibratoMetric *filesOpened = [LibratoMetric metricNamed:@"files.opened" valued:@42 options:nil]; -// Optional values can be set directly on the metric object as well. -// NOTE: The maximum age for any submitted metric is fifteen minutes, as dictated by Librato. -filesOpened.measureTime = [NSDate.date dateByAddingTimeInterval:-10]; +// Create a metric with a specific name and value +LibratoMetric *filesOpened = [LibratoMetric metricNamed:@"files.opened" valued:@42]; +// Add it to the queue to be automatically submitted [librato add:filesOpened]; ``` @@ -56,7 +56,7 @@ Two types of measurement are currently available: counts and groups. These act a This is the default metric type and requires only an NSString name and NSNumber value. ```objective-c -LibratoMetric *metric = [LibratoMetric metricNamed:@"downloads" valued:@42 options:nil]; +LibratoMetric *metric = [LibratoMetric metricNamed:@"downloads" valued:@42]; ``` Additionally, you can provide optional `source` and `measureTime`. The `source` is useful when reviewing data to determine from where measurements with the same name originate. The `measureTime` is automatically generated if not provided but you can set a unique time if you have events that occurred in the past and want to add them to the stack. Metrics must be marked as happening within the last year's time. @@ -66,19 +66,31 @@ Additionally, you can provide optional `source` and `measureTime`. The `source` These values can be provided in the `options` NSDictionary or stated explicitly after the object has been instantiated. ```objective-c -LibratoMetric *metric = [LibratoMetric metricNamed:@"downloads" valued:@42 options:@{@"source": @"the internet", @"measureTime": [NSDate.date dateByAddingTimeInterval:-(3600 * 24)]}]; +NSDate *previousDay = [NSDate.date dateByAddingTimeInterval:-(3600 * 24)]; +LibratoMetric *metric = [LibratoMetric metricNamed:@"downloads" + valued:@42 + options:@{ + @"source": @"the internet", + @"measureTime": previousDay + }]; // or... -LibratoMetric *metric = [LibratoMetric metricNamed:@"downloads" valued:@42 options:nil]; +LibratoMetric *metric = [LibratoMetric metricNamed:@"downloads" valued:@42]; metric.source = @"the internet"; -metric.measureTime = [NSDate.date dateByAddingTimeInterval:-(3600 * 24)] +metric.measureTime = previousDay; ``` Optionally, you can create one or more counters inline with an NSDictionary when adding. ```objective-c -[ add:@{@"downloads": @13, @"plutonium": @{@"value": @238, @"source": @"Russia, with love"}}]; +[ add:@{ + @"downloads": @13, + @"plutonium": @{ + @"value": @238, + @"source": @"Russia, with love" + } + }]; ``` ### Grouping @@ -86,14 +98,14 @@ Optionally, you can create one or more counters inline with an NSDictionary when Groups are aggregated metrics of multiple data points with related, meaningful data. These are created with an array of counter metrics. ```objective-c -LibratoMetric *bagelMetric1 = [LibratoMetric metricNamed:@"bagels" valued:@13 options:nil]; -LibratoMetric *bagelMetric2 = [LibratoMetric metricNamed:@"bagels" valued:@10 options:nil]; -LibratoMetric *bagelMetric3 = [LibratoMetric metricNamed:@"bagels" valued:@9 options:nil]; -LibratoMetric *bagelMetric4 = [LibratoMetric metricNamed:@"bagels" valued:@8 options:nil]; -LibratoMetric *bagelMetric5 = [LibratoMetric metricNamed:@"bagels" valued:@2 options:nil]; -LibratoMetric *bagelMetric6 = [LibratoMetric metricNamed:@"bagels" valued:@1 options:nil]; -LibratoMetric *bagelMetric7 = [LibratoMetric metricNamed:@"bagels" valued:@0 options:nil]; -LibratoMetric *bagelMetric8 = [LibratoMetric metricNamed:@"bagels" valued:@0 options:nil]; +LibratoMetric *bagelMetric1 = [LibratoMetric metricNamed:@"bagels" valued:@13]; +LibratoMetric *bagelMetric2 = [LibratoMetric metricNamed:@"bagels" valued:@10]; +LibratoMetric *bagelMetric3 = [LibratoMetric metricNamed:@"bagels" valued:@9]; +LibratoMetric *bagelMetric4 = [LibratoMetric metricNamed:@"bagels" valued:@8]; +LibratoMetric *bagelMetric5 = [LibratoMetric metricNamed:@"bagels" valued:@2]; +LibratoMetric *bagelMetric6 = [LibratoMetric metricNamed:@"bagels" valued:@1]; +LibratoMetric *bagelMetric7 = [LibratoMetric metricNamed:@"bagels" valued:@0]; +LibratoMetric *bagelMetric8 = [LibratoMetric metricNamed:@"bagels" valued:@0]; NSArray *bagels = @[bagelMetric1, bagelMetric2, bagelMetric3, bagelMetric4, bagelMetric5, bagelMetric6, bagelMetric7, bagelMetric8]; LibratoGaugeMetric *bagelGuage = [LibratoGaugeMetric metricNamed:@"bagel_guage" measurements:bagels]; @@ -125,12 +137,14 @@ If you have a metrics you'd like to add to the queue and trigger an immediate su There's an optional but highly-recommended prefix you can set which will automatically be added to all metric names. This is a great way to isolate data or quickly filter metrics. -# Monitoring Submission Success or Failure +# Submission Success or Failure You can set a blocks to handle the success and failure cases for metric submission. These are referenced when the submission calls back so sporadically setting or `nil`-ling the blocks may lead to unexpected results. ```objective-c -Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" apiKey:@"abc123..." prefix:@""]; +Librato *librato = [Librato.alloc initWithEmail:@"user@somewhere.com" + apiKey:@"abc123..." + prefix:@""]; [libratoInstance setSubmitSuccessBlock:^(NSDictionary *JSON, NSUInteger code) { if (code == 200) { NSLog(@"Successful submission. Response JSON is: %@", JSON); diff --git a/librato-iOS.podspec b/librato-iOS.podspec index 8887649..0f260e6 100644 --- a/librato-iOS.podspec +++ b/librato-iOS.podspec @@ -1,9 +1,9 @@ Pod::Spec.new do |s| s.name = "librato-iOS" - s.version = "0.1.2" + s.version = "1.0.0" s.summary = "Librato library for iOS" s.description = <<-DESC - A simple wrapper for the Librato API with some conveniences for common use cases + A simple, delightful wrapper for the Librato API with conveniences for common use cases DESC s.homepage = "https://github.com/amco/librato-iOS" s.license = { :type => 'MIT', :file => 'LICENSE.md' }