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/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 14e47c0..ef8fec9 100644 --- a/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m +++ b/Demo/librato-iOS Demo/LibratoDemoAppDelegate.m @@ -25,6 +25,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [eventTracker groupDictionaryExample]; [eventTracker groupContextExample]; [eventTracker gaugeMetricExample]; + [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 4f547d1..9f2a10b 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.h +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.h @@ -22,5 +22,9 @@ - (void)groupDictionaryExample; - (void)groupContextExample; - (void)gaugeMetricExample; +- (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 1203bee..7a562a4 100644 --- a/Demo/librato-iOS Demo/LibratoDemoEventTracker.m +++ b/Demo/librato-iOS Demo/LibratoDemoEventTracker.m @@ -49,35 +49,30 @@ + (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)]; - NSLog(@"%@", simpleMetric); - - [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]; - NSLog(@"%@", memoryMetric); - NSLog(@"%@", storageMetric); - - [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"}}]; } @@ -89,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 { @@ -99,7 +94,7 @@ - (void)groupDictionaryExample @"friends": @172 }; NSArray *metrics = [LibratoDemoEventTracker.sharedInstance groupNamed:@"user" valued:valueDict]; - [LibratoDemoEventTracker.sharedInstance submit:metrics]; + [LibratoDemoEventTracker.sharedInstance add:metrics]; } @@ -112,13 +107,38 @@ - (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]]; }]; } /* - Creates a series of counter measurements and submits them as a gague metric + 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 add:@[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 adds them as a gague metric */ - (void)gaugeMetricExample { @@ -132,10 +152,54 @@ - (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]; + [LibratoDemoEventTracker.sharedInstance add:bagelGuage]; +} + + +/* + 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 add:@{@"ua.custom.instances": @1}]; +} + + +/* + 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 add:@[basic, explicit, custom]]; +} + + +- (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 add:[LibratoMetric metricNamed:@"callbacks.test" valued:@123]]; } 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/README.md b/README.md index 10dc63f..cfc62b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ 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. Currently iOS versions 6 and 7 are supported and the wonderful [AFNetworking](https://github.com/AFNetworking/AFNetworking) is used to handle network duties. @@ -12,16 +14,17 @@ 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. -filesOpened.measureTime = [NSDate.date dateByAddingTimeInterval:-10]; +// Create a metric with a specific name and value +LibratoMetric *filesOpened = [LibratoMetric metricNamed:@"files.opened" valued:@42]; -[librato submit:filesOpened]; +// Add it to the queue to be automatically submitted +[librato add:filesOpened]; ``` # Installation @@ -53,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. @@ -63,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 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 @@ -83,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]; @@ -98,10 +113,67 @@ 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. +# 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 add:[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. + +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 diff --git a/librato-iOS.podspec b/librato-iOS.podspec index 5219f3a..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' } @@ -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.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..cbeb90e 100644 --- a/librato-iOS/Classes/LibratoClient.h +++ b/librato-iOS/Classes/LibratoClient.h @@ -17,16 +17,22 @@ typedef void (^ClientFailureBlock)(NSError *error, NSDictionary *JSON); @interface LibratoClient : AFHTTPClient -@property (nonatomic, strong) NSString *APIEndpoint; -@property (nonatomic, strong) NSString *agentIdentifier; +@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; @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 11b4184..33ceeaf 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,22 +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; } -// TODO: Implelement -- (NSString *)agentIdentifier + +- (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 { - return @""; + 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]; @@ -128,10 +219,18 @@ - (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]; 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) { @@ -159,8 +258,7 @@ - (void)flushAuthentication - (NSDictionary *)metrics { - // TODO: Implement - return NSDictionary.dictionary; + return self.queue.queued; } @@ -200,7 +298,11 @@ - (NSString *)persistence - (void)submit:(id)metrics { [self.queue add:metrics]; - [self.queue submit]; + + if (self.isOnline) + { + [self.queue submit]; + } } @@ -258,6 +360,17 @@ - (NSString *)APIEndpoint } +- (NSString *)archivePath +{ + if (!_archivePath) + { + _archivePath = [NSString stringWithFormat:@"%@/%@", NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0], ARCHIVE_FILENAME]; + } + + return _archivePath; +} + + - (LibratoConnection *)connection { if (!_connection) { @@ -303,5 +416,4 @@ - (NSString *)description } - @end 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/LibratoDirectPersister.m b/librato-iOS/Classes/LibratoDirectPersister.m index 0a3a2ab..3736ffb 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,16 +22,17 @@ - (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) { - [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/Classes/LibratoProcessor.h b/librato-iOS/Classes/LibratoProcessor.h index ecc6268..b57bfee 100644 --- a/librato-iOS/Classes/LibratoProcessor.h +++ b/librato-iOS/Classes/LibratoProcessor.h @@ -8,31 +8,34 @@ #import #import "LibratoPersister.h" +#import "MTLModel.h" typedef void(^TimedExecutionBlock)(void); @class LibratoClient, LibratoMetric, LibratoPersister; -@interface LibratoProcessor : NSObject { +@interface LibratoProcessor : MTLModel { NSMutableDictionary *_queued; } @property (nonatomic) NSTimeInterval autosubmitInterval; +@property (nonatomic, strong) NSTimer *autoSubmitTimer; @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 c317b2c..fcfe7c6 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) @@ -68,7 +81,7 @@ - (LibratoMetric *)time:(TimedExecutionBlock)block named:(NSString *)name option return NSClassFromString(type).new; } -- (NSTimeInterval)epochTime ++ (NSTimeInterval)epochTime { return [NSDate.date timeIntervalSince1970]; } @@ -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/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 7632752..d964443 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,27 @@ - (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]) + { + LibratoMetric *metric = metrics; + if (metric.measureTime) + { + [self checkMeasurementTime:metric]; + } + + LibratoMetricCollection *bucket = [self collectionNamed:metric.type]; + [bucket addMetric: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]; @@ -66,16 +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? - if (![self.queued.allKeys containsObject:metric.type]) - { - [self.queued addEntriesFromDictionary:@{metric.type: NSMutableArray.array}]; - } - - [(NSMutableArray *)[self.queued objectForKey:metric.type] addObject:metric.JSON]; + LibratoMetricCollection *bucket = [self collectionNamed:metric.type]; + [bucket addMetric:metric]; }]; [self submitCheck]; @@ -84,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; @@ -96,28 +116,40 @@ - (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; } -- (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"; } @@ -140,7 +172,7 @@ - (BOOL)isEmpty - (void)clear { - self.queued = NSMutableDictionary.dictionary; + [self.queued removeAllObjects]; } @@ -150,15 +182,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; } @@ -187,8 +229,8 @@ - (NSMutableDictionary *)queued - (NSUInteger)size { __block NSUInteger result = 0; - [self.queued enumerateKeysAndObjectsUsingBlock:^(id key, id data, BOOL *stop) { - result += [data count]; + [self.queued enumerateKeysAndObjectsUsingBlock:^(id key, LibratoMetricCollection *collection, BOOL *stop) { + result += collection.models.count; }]; return result; @@ -249,7 +291,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 bc21aea..e17aed6 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; @@ -22,30 +23,37 @@ extern NSString *const LIBRATO_LOCALIZABLE; @interface Librato : NSObject -typedef void (^LibratoMetricContext)(Librato *l); +typedef void (^LibratoMetricContext)(Librato *librato); +typedef void (^LibratoNotificationContext)(NSNotification *notification); @property (nonatomic, strong) LibratoClient *client; @property (nonatomic, strong) NSString *prefix; +@property (nonatomic, strong) dispatch_queue_t queue; + (NSDate *)minimumMeasureTime; - (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; +- (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; - (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; -- (void)submit; +- (id)listenForNotification:(NSString *)named context:(LibratoNotificationContext)context; - (void)submit:(id)metrics; @end diff --git a/librato-iOS/Librato.m b/librato-iOS/Librato.m index d4f9644..4e96a62 100644 --- a/librato-iOS/Librato.m +++ b/librato-iOS/Librato.m @@ -12,25 +12,45 @@ #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 + (NSDate *)minimumMeasureTime { - return [NSDate.date dateByAddingTimeInterval:-(3600*24*365)]; + return [NSDate.date dateByAddingTimeInterval:-(60*15)]; } #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])) { self.prefix = prefix ?: @""; + self.queue = dispatch_queue_create("LibratoQueue", NULL); [self authenticateEmail:email APIKey:apiKey]; + [self trackDefaultMetrics]; } return self; @@ -69,6 +89,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; @@ -87,12 +125,6 @@ - (void)setPersistence:(NSString *)persistence } -- (LibratoConnection *)connection -{ - return self.client.connection; -} - - - (void)getMetric:(NSString *)name options:(NSDictionary *)options { [self.client getMetric:name options:options]; @@ -117,6 +149,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 { @@ -137,14 +181,102 @@ - (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]; +} + + +- (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; +} + + +- (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 add:@[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 add: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 add:versionLevels]; +} + + +- (void)trackLibraryMetrics +{ + [self add:[LibratoMetric metricNamed:@"librato-iOS.version" valued:@(LibratoVersion.version.floatValue)]]; } #pragma mark - Submission -- (void)submit +- (void)add:(id)metrics { - [self.client.queue submit]; + [self.client.queue add:metrics]; } 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..16ee6df 100644 --- a/librato-iOS/Metrics/LibratoGaugeMetric.m +++ b/librato-iOS/Metrics/LibratoGaugeMetric.m @@ -23,18 +23,26 @@ @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; } +- (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])) { self.type = @"gauges"; - [self.data removeObjectForKey:LibratoMetricValueKey]; } return self; @@ -44,27 +52,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 453f420..7b08be6 100644 --- a/librato-iOS/Metrics/LibratoMetric.h +++ b/librato-iOS/Metrics/LibratoMetric.h @@ -7,26 +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; -+ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options; +- (NSDictionary *)JSONDictionary; -- (NSDictionary *)JSON; -- (NSString *)source; -- (void)setSource:(NSString *)source; -- (NSNumber *)value; -- (void)setValue:(NSNumber *)value; ++ (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; @end diff --git a/librato-iOS/Metrics/LibratoMetric.m b/librato-iOS/Metrics/LibratoMetric.m index ec982b3..706aea5 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"; @@ -17,100 +18,124 @@ @implementation LibratoMetric #pragma mark - Lifecycle -+ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value { - return [LibratoMetric.alloc initWithName:name valued:value options:options]; + return [LibratoMetric.alloc initWithName:name valued:value options:nil]; } -- (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options { - if ((self = super.init)) - { - self.data = (options ? options.mutableCopy : @{}.mutableCopy); - self.name = name; - self.value = value ?: @0; - self.source = options[LibratoMetricSourceKey]; - self.type = @"counters"; - } - - return self; + return [LibratoMetric.alloc initWithName:name valued:value options:options]; } -#pragma mark - Properties -- (NSString *)name ++ (instancetype)metricNamed:(NSString *)name valued:(NSNumber *)value source:(NSString *)source measureTime:(NSDate *)date { - return self.data[LibratoMetricNameKey] ?: nil; + return [LibratoMetric.alloc initWithName:name valued:value options:@{ + LibratoMetricSourceKey: source, + LibratoMetricMeasureTimeKey: date + }]; } -- (void)setName:(NSString *)name +- (instancetype)init { - NSAssert(name.length > 0, @"Measurements must be named"); - self.data[LibratoMetricNameKey] = name.sanitizedForMetric; + NSAssert(false, @"You must use initWithName:valued:options: to initialize a LibratoMetric instance"); + self = nil; + + return nil; } -- (NSDate *)measureTime +- (instancetype)initWithName:(NSString *)name valued:(NSNumber *)value options:(NSDictionary *)options { - return self.data[LibratoMetricMeasureTimeKey] ?: nil; + if ((self = super.init)) + { + _name = name; + _value = value ?: @0; + _measureTime = options[LibratoMetricMeasureTimeKey] ?: NSDate.date; + _source = options[LibratoMetricSourceKey] ?: NSNull.null; + _type = @"counters"; + } + + return self; } -- (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; } diff --git a/librato-iOS/Metrics/LibratoMetricCollection.h b/librato-iOS/Metrics/LibratoMetricCollection.h new file mode 100644 index 0000000..cc1c0a7 --- /dev/null +++ b/librato-iOS/Metrics/LibratoMetricCollection.h @@ -0,0 +1,23 @@ +// +// 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)addMetric:(LibratoMetric *)metric; +- (void)removeMetric:(LibratoMetric *)metric; +- (NSMutableArray *)toJSON; + +@end diff --git a/librato-iOS/Metrics/LibratoMetricCollection.m b/librato-iOS/Metrics/LibratoMetricCollection.m new file mode 100644 index 0000000..7657fa7 --- /dev/null +++ b/librato-iOS/Metrics/LibratoMetricCollection.m @@ -0,0 +1,66 @@ +// +// 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 - Model management +- (void)addMetric:(LibratoMetric *)metric +{ + [self.models addObject:metric]; +} + + +- (void)removeMetric:(LibratoMetric *)metric +{ + [self.models removeObject:metric]; +} + + +#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