Classes/TLSLoggingService.m (527 lines of code) (raw):
//
// TLSLoggingService.m
// TwitterLoggingService
//
// Created on 12/11/13.
// Copyright (c) 2016 Twitter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <pthread.h>
#import <TwitterLoggingService/TLSLoggingService+Advanced.h>
#import <TwitterLoggingService/TLSProtocols.h>
#import "TLS_Project.h"
@class TLSLoggingService;
#define TLSCANLOGMODE_ALWAYS (0)
#define TLSCANLOGMODE_CHECKCACHED (1)
#define TLSCANLOGMODE_CHECKFULL (2)
#ifndef TLSCANLOGMODE
#define TLSCANLOGMODE TLSCANLOGMODE_CHECKCACHED
#endif
// Singleton Reference
static TLSLoggingService *sLoggingService;
#if DEBUG
#define SANITIZED_LEVEL(level) (level)
#else
#define SANITIZED_LEVEL(level) ((level) & ~TLSLogLevelMaskDebug)
#endif
static NSString * const kMainThreadName = @"Main";
@interface TLSLoggingService ()
{
dispatch_queue_t _transactionQueue;
dispatch_queue_t _loggingQueue;
CFAbsoluteTime _baseTimestamp;
NSMutableSet<id<TLSOutputStream>> *_streamsM;
#if TLSCANLOGMODE == TLSCANLOGMODE_CHECKCACHED
dispatch_queue_t _quickFilterQueue;
TLSLogLevelMask _quickFilterLevels;
NSMutableSet<NSString *> *_quickFilterOffChannelsM;
NSUInteger _quickFilterOutputStreamCount;
#endif
}
@property (nonatomic, readwrite) NSUInteger maximumSafeMessageLength;
@property (atomic, readwrite, nullable, weak) id<TLSLoggingServiceDelegate> delegate;
// accessible from external queues
- (void)_logDispatchWithLevel:(TLSLogLevel)level
channel:(NSString *)channel
file:(NSString *)file
function:(NSString *)function
line:(NSInteger)line
context:(id)contextObject
options:(TLSLogMessageOptions)options
format:(NSString *)format
arguments:(va_list)arguments TLS_OBJC_DIRECT;
- (BOOL)_canLogWithLevel:(TLSLogLevel)level
channel:(NSString *)channel
context:(id)contextObject TLS_OBJC_DIRECT;
// accessible from any queue except the quickFilter queue
- (void)_nonquickFilter_resetQuickFilter:(NSUInteger)outputStreamCount TLS_OBJC_DIRECT;
// accessible from transaction queue
- (void)_transaction_logExecuteWithTimestamp:(CFAbsoluteTime)timestamp
level:(TLSLogLevel)level
channel:(NSString *)channel
file:(NSString *)file
function:(NSString *)function
line:(NSInteger)line
context:(id)contextObject
threadId:(unsigned int)threadId
threadName:(NSString *)threadName
message:(NSString *)message TLS_OBJC_DIRECT;
- (TLSFilterStatus)_transaction_filterLogStream:(id<TLSOutputStream>)stream
level:(TLSLogLevel)level
channel:(NSString *)channel
context:(id)contextObject TLS_OBJC_DIRECT;
@end
@implementation TLSLoggingService
+ (instancetype)sharedInstance
{
static dispatch_once_t sOnceToken;
dispatch_once(&sOnceToken, ^{
sLoggingService = [[TLSLoggingService alloc] init];
});
return sLoggingService;
}
- (instancetype)init
{
if (self = [super init]) {
_baseTimestamp = CFAbsoluteTimeGetCurrent();
_streamsM = [[NSMutableSet alloc] init];
_loggingQueue = dispatch_queue_create("TLSLoggingService.logging", DISPATCH_QUEUE_SERIAL);
_transactionQueue = dispatch_queue_create("TLSLoggingService.transaction", DISPATCH_QUEUE_SERIAL);
_maximumSafeMessageLength = 0;
#if TLSCANLOGMODE == TLSCANLOGMODE_CHECKCACHED
_quickFilterQueue = dispatch_queue_create("TLSLoggingService.quickFilter", DISPATCH_QUEUE_SERIAL);
_quickFilterLevels = SANITIZED_LEVEL(TLSLogLevelMaskAll);
_quickFilterOutputStreamCount = 0;
_quickFilterOffChannelsM = [[NSMutableSet alloc] init];
#endif
}
return self;
}
- (void)dealloc
{
[self flush];
}
- (void)addOutputStream:(id<TLSOutputStream>)stream
{
if (!stream) {
return;
}
[self dispatchAsynchronousTransaction:^{
[self->_streamsM addObject:stream];
[self _nonquickFilter_resetQuickFilter:self->_streamsM.count];
}];
}
- (void)logWithLevel:(TLSLogLevel)level
channel:(NSString *)channel
file:(NSString *)file
function:(NSString *)function
line:(NSInteger)line
contextObject:(id)contextObject
options:(TLSLogMessageOptions)options
message:(NSString *)message, ...
{
va_list arguments;
va_start(arguments, message);
[self _logDispatchWithLevel:level
channel:channel
file:file
function:function
line:line
context:contextObject
options:options
format:message
arguments:arguments];
va_end(arguments);
}
#pragma mark Private
- (void)_nonquickFilter_resetQuickFilter:(NSUInteger)outputStreamCount
{
#if TLSCANLOGMODE == TLSCANLOGMODE_CHECKCACHED
@autoreleasepool {
dispatch_sync(_quickFilterQueue, ^{
self->_quickFilterLevels = SANITIZED_LEVEL(TLSLogLevelMaskAll);
[self->_quickFilterOffChannelsM removeAllObjects];
self->_quickFilterOutputStreamCount = outputStreamCount;
});
}
#endif
}
- (void)_logDispatchWithLevel:(TLSLogLevel)level
channel:(NSString *)channel
file:(NSString *)file
function:(NSString *)function
line:(NSInteger)line
context:(id)contextObject
options:(TLSLogMessageOptions)options
format:(NSString *)format
arguments:(va_list)arguments
{
if (channel && format) {
const mach_port_t threadId = pthread_mach_thread_np(pthread_self());
NSString * const threadName = TLSCurrentThreadName();
const CFAbsoluteTime timestamp = CFAbsoluteTimeGetCurrent();
NSString * message = [[NSString alloc] initWithFormat:format arguments:arguments];
if (TLS_BITMASK_EXCLUDES_FLAGS(options, TLSLogMessageOptionsIgnoringMaximumSafeMessageLength)) {
const NSUInteger maximumMessageLength = self.maximumSafeMessageLength;
if (maximumMessageLength > 0) {
const NSUInteger length = message.length;
if (length > maximumMessageLength) {
NSUInteger lengthToLog = maximumMessageLength;
const id<TLSLoggingServiceDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(tls_loggingService:lengthToLogForMessageExceedingMaxSafeLength:level:channel:file:function:line:contextObject:message:)]) {
lengthToLog = [delegate tls_loggingService:self
lengthToLogForMessageExceedingMaxSafeLength:maximumMessageLength
level:level
channel:channel
file:file
function:function
line:line
contextObject:contextObject
message:message];
}
if (!lengthToLog) {
return;
} else if (lengthToLog < length) {
message = [message substringToIndex:lengthToLog];
}
}
}
}
[self dispatchAsynchronousTransaction:^{
[self _transaction_logExecuteWithTimestamp:timestamp
level:level
channel:channel
file:file
function:function
line:line
context:contextObject
threadId:threadId
threadName:threadName
message:message];
}];
}
}
- (void)_transaction_logExecuteWithTimestamp:(CFAbsoluteTime)timestamp
level:(TLSLogLevel)level
channel:(NSString *)channel
file:(NSString *)file
function:(NSString *)function
line:(NSInteger)line
context:(id)contextObject
threadId:(unsigned int)threadId
threadName:(NSString *)threadName
message:(NSString *)message
{
if (_streamsM.count > 0) {
const NSTimeInterval elapsedTime = timestamp - _baseTimestamp;
TLSLogMessageInfo *info = [[TLSLogMessageInfo alloc] initWithLevel:level
file:file
function:function
line:line
channel:channel
timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:timestamp]
logLifespan:elapsedTime
threadId:threadId
threadName:threadName
contextObject:contextObject
message:message];
NSMutableSet *permittedStreams = [[NSMutableSet alloc] init];
struct {
unsigned int channel:1;
unsigned int level:1;
unsigned int streamEncountered:1;
} exclusiveFiltering;
exclusiveFiltering.channel = exclusiveFiltering.level = 1;
exclusiveFiltering.streamEncountered = 0;
for (id<TLSOutputStream> stream in _streamsM) {
const TLSFilterStatus status = [self _transaction_filterLogStream:stream
level:level
channel:channel
context:contextObject];
if (TLSFilterStatusOK == status) {
[permittedStreams addObject:stream];
}
if (exclusiveFiltering.channel && TLS_BITMASK_EXCLUDES_FLAGS(status, TLSFilterStatusCannotLogChannel)) {
exclusiveFiltering.channel = 0;
}
if (exclusiveFiltering.level && TLS_BITMASK_EXCLUDES_FLAGS(status, TLSFilterStatusCannotLogLevel)) {
exclusiveFiltering.level = 0;
}
exclusiveFiltering.streamEncountered = 1;
}
if (permittedStreams.count > 0) {
dispatch_async(_loggingQueue, ^{
@autoreleasepool {
for (id<TLSOutputStream> stream in permittedStreams) {
[stream tls_outputLogInfo:info];
}
}
});
}
#if TLSCANLOGMODE == TLSCANLOGMODE_CHECKCACHED
else if (exclusiveFiltering.streamEncountered && (exclusiveFiltering.channel || exclusiveFiltering.level)) {
@autoreleasepool {
dispatch_sync(_quickFilterQueue, ^{
if (exclusiveFiltering.channel) {
[self->_quickFilterOffChannelsM addObject:channel];
}
if (exclusiveFiltering.level) {
self->_quickFilterLevels &= ~(1 << level);
}
});
}
}
#endif
}
}
- (TLSFilterStatus)_transaction_filterLogStream:(id<TLSOutputStream>)stream
level:(TLSLogLevel)level
channel:(NSString *)channel
context:(id)contextObject
{
const TLSLogLevelMask mask = SANITIZED_LEVEL(TLSLogLevelMaskAll);
// Can we log to this level?
if (TLS_BITMASK_EXCLUDES_FLAGS(mask, (1 << level))) {
return TLSFilterStatusCannotLogLevel;
}
if ([stream respondsToSelector:@selector(tls_shouldFilterLevel:channel:contextObject:)]) {
return [stream tls_shouldFilterLevel:level
channel:channel
contextObject:contextObject];
}
return TLSFilterStatusOK;
}
- (BOOL)_canLogWithLevel:(TLSLogLevel)level
channel:(NSString *)channel
context:(id)contextObject
{
#if TLSCANLOGMODE == TLSCANLOGMODE_CHECKCACHED
__block BOOL canLog = (nil != channel);
if (canLog) {
@autoreleasepool {
dispatch_sync(_quickFilterQueue, ^{
canLog = (self->_quickFilterOutputStreamCount > 0)
&& TLS_BITMASK_HAS_SUBSET_FLAGS(self->_quickFilterLevels, (1 << level))
&& ![self->_quickFilterOffChannelsM containsObject:channel];
});
}
}
return canLog;
#elif TLSCANLOGMODE == TLSCANLOGMODE_CHECKFULL
__block BOOL canLog = NO;
[self dispatchSynchronousTransaction:^{
for (id<TLSOutputStream> stream in self->_streamsM) {
TLSFilterStatus status = [self _transaction_filterLogStream:stream
level:level
channel:channel
context:contextObject];
if (TLSFilterStatusOK == status) {
canLog = YES;
break;
}
}
}];
return canLog;
#else // TLSCANLOGMODE == TLSCANLOGMODE_ALWAYS
#if DEBUG
return (level <= TLSLogLevelDebug);
#else
return (level < TLSLogLevelDebug);
#endif
#endif
}
@end
@implementation TLSLoggingService (Advanced)
- (NSDate *)startupTimestamp
{
return [NSDate dateWithTimeIntervalSinceReferenceDate:_baseTimestamp];
}
// see `@implementation TLSLoggingService` for `- (void)addOutputStream:(id<TLSOutputStream>)stream`
- (void)removeOutputStream:(id<TLSOutputStream>)stream
{
if (!stream) {
return;
}
[self dispatchAsynchronousTransaction:^{
if ([self->_streamsM containsObject:stream]) {
[self->_streamsM removeObject:stream];
[self _nonquickFilter_resetQuickFilter:self->_streamsM.count];
dispatch_async(self->_loggingQueue, ^{
@autoreleasepool {
if ([stream respondsToSelector:@selector(tls_flush)]) {
[stream tls_flush];
}
}
});
}
}];
}
- (NSSet *)outputStreams
{
__block NSSet *streams;
[self dispatchSynchronousTransaction:^{
streams = [self->_streamsM copy];
}];
return streams;
}
- (void)updateOutputStream:(id<TLSOutputStream>)stream
{
if (!stream) {
return;
}
#if TLSCANLOGMODE == TLSCANLOGMODE_CHECKCACHED
dispatch_block_t block = ^{
if ([self->_streamsM containsObject:stream]) {
[self _nonquickFilter_resetQuickFilter:self->_streamsM.count];
}
};
[self dispatchAsynchronousTransaction:block];
#endif
}
- (void)dispatchSynchronousTransaction:(dispatch_block_t NS_NOESCAPE)block
{
@autoreleasepool {
dispatch_sync(_transactionQueue, block);
}
}
- (void)dispatchAsynchronousTransaction:(dispatch_block_t)block
{
dispatch_async(_transactionQueue, ^{
@autoreleasepool {
block();
}
});
}
- (void)flush
{
// get all log message transactions onto the logging queue
// and get our output streams from the transaction queue
__block NSSet *streams = nil;
[self dispatchSynchronousTransaction:^{
streams = [self->_streamsM copy];
}];
// get all log messages off the logging queue and then flush all output streams
@autoreleasepool {
dispatch_sync(_loggingQueue, ^{
for (id<TLSOutputStream> stream in streams) {
if ([stream respondsToSelector:@selector(tls_flush)]) {
[stream tls_flush];
}
}
});
}
/*
NOTE:
We do not do the following:
dispatch_sync(_transactionQueue, ^{
dispatch_sync(_loggingQueue, ^{
for (id<TLSOutputStream> stream in _streamsM) {
if ([stream respondsToSelector:@selector(tls_flush)]) {
[stream tls_flush];
}
}
});
});
});
This would expose the potential for a deadlock if any queued logging statements would lead to a
log message OR if any output stream's tls_flush method would lead to a log message.
By "flushing" the transaction queue first we don't have to block that queue with the completion
of the logging queue. The only place where we expose a synchronous block executing code
outside TLSLoggingService on the transaction queue is with the TLSFiltering method
implementations which MUST NOT do anything that interacts with the TLSLoggingService.
*/
}
- (NSSet<id<TLSOutputStream, TLSDataRetrieval>> *)outputStreamsThatSupportLoggedDataRetrieval
{
NSSet *streams = self.outputStreams;
NSMutableSet<id<TLSOutputStream, TLSDataRetrieval>> *dataRetrievalStream = [[NSMutableSet alloc] init];
for (id<TLSOutputStream> stream in streams) {
if ([stream conformsToProtocol:@protocol(TLSDataRetrieval)]) {
[dataRetrievalStream addObject:(id<TLSOutputStream, TLSDataRetrieval>)stream];
}
}
return [dataRetrievalStream copy];
}
- (NSData *)retrieveLoggedDataFromOutputStream:(id<TLSOutputStream, TLSDataRetrieval>)stream
maxBytes:(NSUInteger)maxBytes
{
if (![stream conformsToProtocol:@protocol(TLSDataRetrieval)]) {
return nil;
}
__block NSData *data;
@autoreleasepool {
dispatch_sync(_loggingQueue, ^{
data = [stream tls_retrieveLoggedData:maxBytes];
});
}
return data;
}
@end
void TLSvaLog(TLSLoggingService *service,
TLSLogLevel level,
NSString *channel,
NSString *file,
NSString *function,
NSInteger line,
id contextObject,
TLSLogMessageOptions options,
NSString *format,
va_list arguments)
{
[(service ?: sLoggingService) _logDispatchWithLevel:level
channel:channel
file:file
function:function
line:line
context:contextObject
options:options
format:format
arguments:arguments];
}
void TLSLogEx(TLSLoggingService *service,
TLSLogLevel level,
NSString *channel,
NSString *file,
NSString *function,
NSInteger line,
id contextObject,
TLSLogMessageOptions options,
NSString *format, ...)
{
va_list arguments;
va_start(arguments, format);
TLSvaLog(service,
level,
channel,
file,
function,
line,
contextObject,
options,
format,
arguments);
va_end(arguments);
}
void TLSLogString(TLSLoggingService *service,
TLSLogLevel level,
NSString *channel,
NSString *file,
NSString *function,
NSInteger line,
id contextObject,
TLSLogMessageOptions options,
NSString *message)
{
TLSLogEx(service,
level,
channel,
file,
function,
line,
contextObject,
options,
@"%@",
message);
}
BOOL TLSCanLog(TLSLoggingService *service,
TLSLogLevel level,
NSString *channel,
id contextObject)
{
return [(service ?: sLoggingService) _canLogWithLevel:level
channel:channel
context:contextObject];
}
NSString *TLSCurrentThreadName()
{
if ([NSThread isMainThread]) {
return kMainThreadName;
}
NSString *name = nil;
name = [NSThread currentThread].name;
if (name.length > 0) {
return name;
}
const char *nameCStr = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
if (nameCStr) {
name = @(nameCStr);
if (name.length > 0) {
return name;
}
}
do
{
const size_t nameLen = NAME_MAX + 1;
char buffer[nameLen];
if (0 == pthread_getname_np(pthread_self(), buffer, nameLen)) {
name = @(buffer);
if (name.length > 0) {
return name;
}
}
} while (0);
return nil;
}