FBControlCore/Utility/FBLoggingWrapper.m (224 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBLoggingWrapper.h"
@interface FBLoggingWrapper ()
@property (nonatomic, strong, readonly) id wrappedObject;
@property (nonatomic, strong, readonly) id<FBEventReporter> eventReporter;
@property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger;
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, assign, readonly) BOOL simplifiedNaming;
@end
@implementation FBLoggingWrapper
#pragma mark Initializers
+ (instancetype)wrap:(id)wrappedObject simplifiedNaming:(BOOL)simplifiedNaming eventReporter:(nullable id<FBEventReporter>)eventReporter logger:(nullable id<FBControlCoreLogger>)logger
{
return [[self alloc] initWithWrappedObject:wrappedObject simplifiedNaming:simplifiedNaming eventReporter:eventReporter logger:logger];
}
- (instancetype)initWithWrappedObject:(id)wrappedObject simplifiedNaming:(BOOL)simplifiedNaming eventReporter:(nullable id<FBEventReporter>)eventReporter logger:(nullable id<FBControlCoreLogger>)logger
{
self = [super init];
if (!self) {
return nil;
}
_wrappedObject = wrappedObject;
_simplifiedNaming = simplifiedNaming;
_queue = dispatch_queue_create("com.facebook.fbcontrolcore.logging_wrapper", DISPATCH_QUEUE_SERIAL);
_eventReporter = eventReporter;
_logger = logger;
return self;
}
#pragma mark Forwarding
- (BOOL)respondsToSelector:(SEL)selector
{
return [super respondsToSelector:selector] || [self.wrappedObject respondsToSelector:selector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
return [super methodSignatureForSelector:selector] ?: [self.wrappedObject methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if ([self.wrappedObject respondsToSelector:invocation.selector]) {
[self runInvocation:invocation];
} else {
[super forwardInvocation:invocation];
}
}
- (void)runInvocation:(NSInvocation *)invocation
{
// Extract information about the start of the call.
NSDate *startDate = NSDate.date;
NSString *methodName = [self.class methodName:invocation simplifiedNaming:self.simplifiedNaming];
NSArray<NSString *> *descriptionOfArguments = [self.class descriptionOfArguments:invocation];
FBEventReporterSubject *beforeSubject = [self.class subjectForBeforeInvocation:methodName descriptionOfArguments:descriptionOfArguments logger:self.logger];
[self.eventReporter report:beforeSubject];
// Extract the first method argument and retain it, if it exists
id firstMethodArgument = nil;
if (invocation.methodSignature.numberOfArguments > 2 && strcmp([invocation.methodSignature getArgumentTypeAtIndex:2], "@") == 0) {
__unsafe_unretained id argument = nil;
[invocation getArgument:&argument atIndex:2];
firstMethodArgument = argument;
}
// Invoke on the Future Handler on the appropriate queue.
[invocation invokeWithTarget:self.wrappedObject];
void *returnValue = NULL;
[invocation getReturnValue:&returnValue];
FBFuture *future = (__bridge FBFuture *)(returnValue);
// Log the end of the call when the future resolves.
if ([future isKindOfClass:FBFuture.class]) {
[future onQueue:self.queue notifyOfCompletion:^(FBFuture *completedFuture) {
FBEventReporterSubject *afterSubject = [self.class subjectAfterCompletion:completedFuture methodName:methodName descriptionOfArguments:descriptionOfArguments startDate:startDate firstMethodArgument:firstMethodArgument logger:self.logger];
[self.eventReporter report:afterSubject];
}];
}
}
#pragma mark - Subjects
+ (FBEventReporterSubject *)subjectForBeforeInvocation:(NSString *)methodName descriptionOfArguments:(NSArray<NSString *> *)descriptionOfArguments logger:(id<FBControlCoreLogger>)logger
{
[logger.info logFormat:@"%@ called with: %@", methodName, [FBCollectionInformation oneLineDescriptionFromArray:descriptionOfArguments]];
return [FBEventReporterSubject subjectForStartedCall:methodName arguments:descriptionOfArguments];
}
+ (FBEventReporterSubject *)subjectAfterCompletion:(FBFuture *)future methodName:(NSString *)methodName descriptionOfArguments:(NSArray<NSString *> *)descriptionOfArguments startDate:(NSDate *)startDate firstMethodArgument:(id)firstMethodArgument logger:(id<FBControlCoreLogger>)logger
{
NSTimeInterval duration = [NSDate.date timeIntervalSinceDate:startDate];
NSError *error = future.error;
NSNumber *size = nil;
if ([firstMethodArgument respondsToSelector:@selector(bytesTransferred)]) {
size = @([firstMethodArgument bytesTransferred]);
}
if (error) {
NSString *message = error.localizedDescription;
[logger.debug logFormat:@"%@ failed with: %@", methodName, message];
return [FBEventReporterSubject subjectForFailingCall:methodName duration:duration message:message size:size arguments:descriptionOfArguments];
} else {
[logger.debug logFormat:@"%@ succeeded", methodName];
return [FBEventReporterSubject subjectForSuccessfulCall:methodName duration:duration size:size arguments:descriptionOfArguments];
}
}
#pragma mark - NSInvocation inspection
+ (NSString *)methodName:(NSInvocation *)invocation simplifiedNaming:(BOOL)simplifiedNaming
{
// This will log the first argument in the method name, so method names should be unique relative to the first component within the selector
if (simplifiedNaming) {
return [NSStringFromSelector(invocation.selector) componentsSeparatedByString:@":"][0];
}
// Otherwise log the entire selector
return NSStringFromSelector(invocation.selector);
}
+ (NSArray<NSString *> *)descriptionOfArguments:(NSInvocation *)invocation
{
NSMutableArray<NSString *> *descriptions = NSMutableArray.array;
for (int index = 2; index < (int) invocation.methodSignature.numberOfArguments; index++) {
NSString *description = [self descriptionForAgumentAtIndex:index inInvoation:invocation];
if (description.length > 100) {
description = [NSString stringWithFormat:@"%@...", [description substringToIndex:100]];
}
[descriptions addObject:description];
}
return descriptions;
}
+ (NSString *)descriptionForAgumentAtIndex:(int)index inInvoation:(NSInvocation *)invocation
{
NSString *type = [NSString stringWithUTF8String:[invocation.methodSignature getArgumentTypeAtIndex:(NSUInteger)index]];
if ([type isEqualToString:@"c"]) {
char argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%c", argument];
}
if ([type isEqualToString:@"i"]) {
int argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%d", argument];
}
if ([type isEqualToString:@"s"]) {
short argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%d", argument];
}
if ([type isEqualToString:@"l"]) {
long argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%ld", argument];
}
if ([type isEqualToString:@"q"]) {
long long argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%lld", argument];
}
if ([type isEqualToString:@"C"]) {
unsigned char argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%c", argument];
}
if ([type isEqualToString:@"I"]) {
unsigned int argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%d", argument];
}
if ([type isEqualToString:@"S"]) {
unsigned short argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%d", argument];
}
if ([type isEqualToString:@"L"]) {
unsigned long argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%ld", argument];
}
if ([type isEqualToString:@"Q"]) {
unsigned long long argument = 0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%lld", argument];
}
if ([type isEqualToString:@"f"]) {
float argument = 0.0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%f", argument];
}
if ([type isEqualToString:@"d"]) {
double argument = 0.0;
[invocation getArgument:&argument atIndex:index];
return [NSString stringWithFormat:@"%f", argument];
}
if ([type isEqualToString:@"v"]) {
return @"void";
}
if ([type isEqualToString:@"@"]) {
__unsafe_unretained id argument = nil;
[invocation getArgument:&argument atIndex:index];
return [self descriptionForObject:argument];
}
return @"Unrecognised type";
}
+ (NSString *)descriptionForObject:(NSObject *)object
{
if ([object isKindOfClass:NSString.class]) {
return [(NSString *)object description];
}
if ([object isKindOfClass:NSData.class]) {
NSData *data = (NSData *)object;
return [NSString stringWithFormat:@"NSData of length %lu", (unsigned long)data.length];
}
if (object == nil || [object isKindOfClass:NSNull.class]) {
return @"null";
}
if ([object isKindOfClass:NSArray.class]) {
return [self implodeDescription:(NSSet<id> *)object prefix:@"NSArray"];
}
if ([object isKindOfClass:NSSet.class]) {
return [self implodeDescription:(NSSet<id> *)object prefix:@"NSSet"];
}
if ([object isKindOfClass:NSDictionary.class]) {
NSDictionary<id, id> *dict = (NSDictionary<id, id> *)object;
NSMutableString *description = NSMutableString.string;
[description appendString:@"NSDictionary{"];
for (NSObject *key in dict) {
[description appendFormat:@"%@: %@", [self descriptionForObject:key], [self descriptionForObject:dict[key]]];
[description appendString:@", "];
}
[description appendString:@"}"];
return description;
}
return object.description;
}
+ (NSString *)implodeDescription:(id<NSFastEnumeration>)container prefix:(NSString *)prefix
{
NSMutableArray<NSString *> *descriptions = NSMutableArray.array;
for (id inner in container) {
[descriptions addObject:[self descriptionForObject:inner]];
}
return [NSString stringWithFormat:@"%@[%@]", prefix, [descriptions componentsJoinedByString:@", "]];
}
@end