Skip to content

Commit bbd0d30

Browse files
authored
Implement log storage (#2215)
* Implement log storage Includes tests and categories on existing classes to add testing functionality * Better error handling in tye log storage * Style and pod lib lint fixes * Add missing comment
1 parent 9b5742c commit bbd0d30

File tree

9 files changed

+469
-5
lines changed

9 files changed

+469
-5
lines changed

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogStorage.h

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,35 @@
1616

1717
#import <Foundation/Foundation.h>
1818

19-
/** Manages the storage and prioritizing of logs. */
20-
@interface GDLLogStorage : NSObject
19+
@class GDLLogEvent;
20+
21+
NS_ASSUME_NONNULL_BEGIN
22+
23+
/** Manages the storage of logs. This class is thread-safe. */
24+
@interface GDLLogStorage : NSObject <NSSecureCoding>
25+
26+
/** Creates and/or returns the storage singleton.
27+
*
28+
* @return The storage singleton.
29+
*/
30+
+ (instancetype)sharedInstance;
31+
32+
/** Stores log.extensionBytes into a shared on-device folder and tracks the log via its hash and
33+
* logTarget properties.
34+
*
35+
* @note The log param is expected to be deallocated during this method.
36+
*
37+
* @param log The log to store.
38+
*/
39+
- (void)storeLog:(GDLLogEvent *)log;
40+
41+
/** Removes the corresponding log file from disk.
42+
*
43+
* @param logHash The hash value of the original log.
44+
* @param logTarget The logTarget of the original log.
45+
*/
46+
- (void)removeLog:(NSNumber *)logHash logTarget:(NSNumber *)logTarget;
2147

2248
@end
49+
50+
NS_ASSUME_NONNULL_END

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogStorage.m

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,182 @@
1515
*/
1616

1717
#import "GDLLogStorage.h"
18+
#import "GDLLogStorage_Private.h"
19+
20+
#import <GoogleDataLogger/GDLLogPrioritizer.h>
21+
22+
#import "GDLConsoleLogger.h"
23+
#import "GDLLogEvent_Private.h"
24+
#import "GDLRegistrar_Private.h"
25+
#import "GDLUploader.h"
26+
27+
/** Creates and/or returns a singleton NSString that is the shared logging path.
28+
*
29+
* @return The SDK logging path.
30+
*/
31+
static NSString *GDLStoragePath() {
32+
static NSString *archivePath;
33+
static dispatch_once_t onceToken;
34+
dispatch_once(&onceToken, ^{
35+
NSString *cachePath =
36+
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
37+
archivePath = [NSString stringWithFormat:@"%@/google-sdks-logs", cachePath];
38+
});
39+
return archivePath;
40+
}
1841

1942
@implementation GDLLogStorage
2043

44+
+ (instancetype)sharedInstance {
45+
static GDLLogStorage *sharedStorage;
46+
static dispatch_once_t onceToken;
47+
dispatch_once(&onceToken, ^{
48+
sharedStorage = [[GDLLogStorage alloc] init];
49+
});
50+
return sharedStorage;
51+
}
52+
53+
- (instancetype)init {
54+
self = [super init];
55+
if (self) {
56+
_storageQueue = dispatch_queue_create("com.google.GDLLogStorage", DISPATCH_QUEUE_SERIAL);
57+
_logHashToLogFile = [[NSMutableDictionary alloc] init];
58+
_logTargetToLogFileSet = [[NSMutableDictionary alloc] init];
59+
}
60+
return self;
61+
}
62+
63+
- (void)storeLog:(GDLLogEvent *)log {
64+
[self createLogDirectoryIfNotExists];
65+
66+
// This is done to ensure that log is deallocated at the end of the ensuing block.
67+
__block GDLLogEvent *shortLivedLog = log;
68+
__weak GDLLogEvent *weakShortLivedLog = log;
69+
log = nil;
70+
71+
dispatch_async(_storageQueue, ^{
72+
// Check that a backend implementation is available for this logTarget.
73+
NSInteger logTarget = shortLivedLog.logTarget;
74+
75+
// Check that a log prioritizer is available for this logTarget.
76+
id<GDLLogPrioritizer> logPrioritizer =
77+
[GDLRegistrar sharedInstance].logTargetToPrioritizer[@(logTarget)];
78+
NSAssert(logPrioritizer, @"There's no scorer registered for the given logTarget.");
79+
80+
// Write the extension bytes to disk, get a filename.
81+
NSAssert(shortLivedLog.extensionBytes, @"The log should have been serialized to bytes");
82+
NSAssert(shortLivedLog.extension == nil, @"The original log proto should be removed");
83+
NSURL *logFile =
84+
[self saveLogProtoToDisk:shortLivedLog.extensionBytes logHash:shortLivedLog.hash];
85+
86+
// Add log to tracking collections.
87+
[self addLogToTrackingCollections:shortLivedLog logFile:logFile];
88+
89+
// Check the QoS, if it's high priority, notify the log target that it has a high priority log.
90+
if (shortLivedLog.qosTier == GDLLogQoSFast) {
91+
NSSet<NSURL *> *allLogsForLogTarget = self.logTargetToLogFileSet[@(logTarget)];
92+
[[GDLUploader sharedInstance] forceUploadLogs:allLogsForLogTarget target:logTarget];
93+
}
94+
95+
// Have the prioritizer prioritize the log, enforcing that they do not retain it.
96+
@autoreleasepool {
97+
[logPrioritizer prioritizeLog:shortLivedLog];
98+
shortLivedLog = nil;
99+
}
100+
if (weakShortLivedLog) {
101+
GDLLogError(GDLMCELogEventWasIllegallyRetained, @"%@",
102+
@"A LogEvent should not be retained outside of storage.");
103+
};
104+
});
105+
}
106+
107+
- (void)removeLog:(NSNumber *)logHash logTarget:(NSNumber *)logTarget {
108+
dispatch_async(_storageQueue, ^{
109+
NSURL *logFile = self.logHashToLogFile[logHash];
110+
111+
// Remove from disk, first and foremost.
112+
NSError *error;
113+
[[NSFileManager defaultManager] removeItemAtURL:logFile error:&error];
114+
NSAssert(error == nil, @"There was an error removing a logFile: %@", error);
115+
116+
// Remove from the tracking collections.
117+
[self.logHashToLogFile removeObjectForKey:logHash];
118+
NSMutableSet<NSURL *> *logFiles = self.logTargetToLogFileSet[logTarget];
119+
NSAssert(logFiles, @"There wasn't a logSet for this logTarget.");
120+
[logFiles removeObject:logFile];
121+
// It's fine to not remove the set if it's empty.
122+
});
123+
}
124+
125+
#pragma mark - Private helper methods
126+
127+
/** Creates the log directory if it does not exist. */
128+
- (void)createLogDirectoryIfNotExists {
129+
NSError *error;
130+
BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:GDLStoragePath()
131+
withIntermediateDirectories:YES
132+
attributes:0
133+
error:&error];
134+
if (!result || error) {
135+
GDLLogError(GDLMCEDirectoryCreationError, @"Error creating the directory: %@", error);
136+
}
137+
}
138+
139+
/** Saves the log's extensionBytes to a file using NSData mechanisms.
140+
*
141+
* @note This method should only be called from a method within a block on _storageQueue to maintain
142+
* thread safety.
143+
*
144+
* @param logProtoBytes The extensionBytes of the log, presumably proto bytes.
145+
* @param logHash The hash value of the log.
146+
* @return The filename
147+
*/
148+
- (NSURL *)saveLogProtoToDisk:(NSData *)logProtoBytes logHash:(NSUInteger)logHash {
149+
NSString *storagePath = GDLStoragePath();
150+
NSString *logFile = [NSString stringWithFormat:@"log-%lu", (unsigned long)logHash];
151+
NSURL *logFilePath = [NSURL fileURLWithPath:[storagePath stringByAppendingPathComponent:logFile]];
152+
153+
BOOL writingSuccess = [logProtoBytes writeToURL:logFilePath atomically:YES];
154+
if (!writingSuccess) {
155+
GDLLogError(GDLMCEFileWriteError, @"A log file could not be written: %@", logFilePath);
156+
}
157+
158+
return logFilePath;
159+
}
160+
161+
/** Adds the log to internal collections in order to help track the log.
162+
*
163+
* @note This method should only be called from a method within a block on _storageQueue to maintain
164+
* thread safety.
165+
*
166+
* @param log The log to track.
167+
* @param logFile The file the log has been saved to.
168+
*/
169+
- (void)addLogToTrackingCollections:(GDLLogEvent *)log logFile:(NSURL *)logFile {
170+
NSInteger logTarget = log.logTarget;
171+
self.logHashToLogFile[@(log.hash)] = logFile;
172+
NSMutableSet<NSURL *> *logs = self.logTargetToLogFileSet[@(logTarget)];
173+
if (logs) {
174+
[logs addObject:logFile];
175+
} else {
176+
NSMutableSet<NSURL *> *logSet = [NSMutableSet setWithObject:logFile];
177+
self.logTargetToLogFileSet[@(logTarget)] = logSet;
178+
}
179+
}
180+
181+
#pragma mark - NSSecureCoding
182+
183+
+ (BOOL)supportsSecureCoding {
184+
return YES;
185+
}
186+
187+
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
188+
// TODO
189+
return [self.class sharedInstance];
190+
}
191+
192+
- (void)encodeWithCoder:(NSCoder *)aCoder {
193+
// TODO
194+
}
195+
21196
@end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2018 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import "GDLLogStorage.h"
18+
19+
@interface GDLLogStorage ()
20+
21+
/** The queue on which all storage work will occur. */
22+
@property(nonatomic) dispatch_queue_t storageQueue;
23+
24+
/** A map of log hash values to log file on-disk URLs. */
25+
@property(nonatomic) NSMutableDictionary<NSNumber *, NSURL *> *logHashToLogFile;
26+
27+
/** A map of logTargets to a set of log hash values. */
28+
@property(nonatomic)
29+
NSMutableDictionary<NSNumber *, NSMutableSet<NSURL *> *> *logTargetToLogFileSet;
30+
31+
@end

GoogleDataLogger/GoogleDataLogger/DependencyWrappers/GDLConsoleLogger.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,24 @@ static GULLoggerService kGDLConsoleLogger = @"[GoogleDataLogger]";
2424
*
2525
* Prefixes:
2626
* - MCW => MessageCodeWarning
27+
* - MCE => MessageCodeError
2728
*/
2829
typedef NS_ENUM(NSInteger, GDLMessageCode) {
2930

3031
/** For warning messages concerning transform: not being implemented by a log transformer. */
3132
GDLMCWTransformerDoesntImplementTransform = 1,
3233

3334
/** For warning messages concerning protoBytes: not being implemented by a log extension. */
34-
GDLMCWExtensionMissingBytesImpl = 2
35+
GDLMCWExtensionMissingBytesImpl = 2,
36+
37+
/** For error messages concerning a GDLLogEvent living past the storeLog: invocation. */
38+
GDLMCELogEventWasIllegallyRetained = 1000,
39+
40+
/** For error messages concerning the creation of a directory failing. */
41+
GDLMCEDirectoryCreationError = 1001,
42+
43+
/** For error messages concerning the writing of a log file. */
44+
GDLMCEFileWriteError = 1002
3545
};
3646

3747
/** */
@@ -49,3 +59,9 @@ FOUNDATION_EXTERN void GDLLogWarning(GDLMessageCode messageCode,
4959
#define GDLLogWarning(MESSAGE_CODE, MESSAGE_FORMAT, ...) \
5060
GULLogWarning(kGDLConsoleLogger, YES, GDLMessageCodeEnumToString(MESSAGE_CODE), MESSAGE_FORMAT, \
5161
__VA_ARGS__);
62+
63+
// A define to wrap GULLogError with slightly more convenient usage and a failing assert.
64+
#define GDLLogError(MESSAGE_CODE, MESSAGE_FORMAT, ...) \
65+
GULLogError(kGDLConsoleLogger, YES, GDLMessageCodeEnumToString(MESSAGE_CODE), MESSAGE_FORMAT, \
66+
__VA_ARGS__); \
67+
NSAssert(NO, MESSAGE_FORMAT, __VA_ARGS__);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2018 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
19+
#import "GDLLogStorage.h"
20+
#import "GDLLogStorage_Private.h"
21+
22+
NS_ASSUME_NONNULL_BEGIN
23+
24+
/** Testing-only methods for GDLLogStorage. */
25+
@interface GDLLogStorage (Testing)
26+
27+
/** Resets the properties of the singleon, but does not reallocate a new singleton. This also
28+
* doesn't remove stored files from disk.
29+
*/
30+
- (void)reset;
31+
32+
@end
33+
34+
NS_ASSUME_NONNULL_END
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2018 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import "GDLLogStorage+Testing.h"
18+
19+
@implementation GDLLogStorage (Testing)
20+
21+
- (void)reset {
22+
dispatch_sync(self.storageQueue, ^{
23+
[self.logTargetToLogFileSet removeAllObjects];
24+
[self.logHashToLogFile removeAllObjects];
25+
});
26+
}
27+
28+
@end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2018 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import "GDLRegistrar.h"
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
21+
/** Testing-only methods for GDLRegistrar. */
22+
@interface GDLRegistrar (Testing)
23+
24+
/** Resets the properties of the singleon, but does not reallocate a new singleton. */
25+
- (void)reset;
26+
27+
@end
28+
29+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)