77

Why the console output shows incomplete in Xcode 8 / iOS 10?

enter image description here

iPeta
  • 822
  • 1
  • 6
  • 9

8 Answers8

75

A temporary solution, just redefine all NSLOG to printf in a global header file.

#define NSLog(FORMAT, ...) printf("%s\n", [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);
xfdai
  • 2,735
  • 1
  • 19
  • 18
  • Worked for me also :) – Ankit Nov 08 '16 at 05:04
  • 2
    My NSLog output was truncating my serialised JSON NSData. Quite annoying. This worked for me too. I'd just suggest taking the semi-colon off the end if pasting in to the top of you .m file. – Carl Hine Dec 21 '17 at 09:27
  • and if you want the date you can do something like this: printf("%s %s\n", [[[NSDate date] description] UTF8String], [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]) – matt bezark Apr 09 '18 at 23:22
  • GREAT SOLUTION! You made my day! – floki1 Jul 09 '23 at 14:40
54

In iOS 10 & Xcode 8, Apple switched from the good old ASL (Apple System Log) to a new logging system called Unified logging. NSLog calls are in fact delegating to new os_log API's. (source: https://developer.apple.com/reference/os/logging):

Important

Unified logging is available in iOS 10.0 and later, macOS 10.12 and later, tvOS 10.0 and later, and watchOS 3.0 and later, and supersedes ASL (Apple System Logger) and the Syslog APIs. Historically, log messages were written to specific locations on disk, such as /etc/system.log. The unified logging system stores messages in memory and in a data store, rather than writing to text-based log files.

And

Important

Log message lines greater than the system’s maximum message length are truncated when stored by the logging system. Complete messages are visible when using the log command-line tool to view a live stream of activity. Bear in mind, however, that streaming log data is an expensive activity.

The "system’s maximum message length" limitation is revealed in the SDK's header to be 1024 characters for formatted variables, as noted by @Hot_Leaks (source: <os/log.h>):

/*!  
 * @function os_log  
 *   
 * ...  
 *  
 * There is a physical cap of 1024 bytes per log line for dynamic content,  
 * such as %s and %@, that can be written to the persistence store.  
 * All content exceeding the limit will be truncated before it is  
 * written to disk.  
 *
 * ... 
 *
 */  
#define os_log(log, format, ...)    os_log_with_type(log, OS_LOG_TYPE_DEFAULT, format, ##__VA_ARGS__)

Since the buffer size limitation seems to be hard-coded into libsystem_trace.dylib, I don't see a way around it but to print a string literal instead of a formatted variable (%@), or split the formatted string variables to < 1024 strings.

printf will work during debugging, since the debugger (Xcode) shows the process's out / error streams, but it will not be sent to the device log itself. This means that xfdai's solution will not help you when using other log applications such as macOS's Console App, or with issue's emerging on non-debugged applications (such as AppStore application running on customer's device).


Extending xfdai's answer to deployed applications

In deployed applications / non-debug builds, there's no way to see either NSLogs or printfs.

The only way to have messages printed directly to the device log (which can be accessed using Xcode -> Window -> Devices, mac's Console App or 3rd party utilities such as deviceconsole) is calling os_log API's (which is the successor of ASL used since iOS 10).

Here's a global header file I'm using to redefine NSLog as a call to _os_log_internal on iOS 10:

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#endif

#import <os/object.h>
#import <os/activity.h>

/*
 *  System Versioning Preprocessor Macros
 */

#define SYSTEM_VERSION_EQUAL_TO(v)                  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
#define SYSTEM_VERSION_GREATER_THAN(v)              ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v)                 ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v)     ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

// os_log is only supported when compiling with Xcode 8.
// Check if iOS version > 10 and the _os_log_internal symbol exists,
// load it dynamically and call it.
// Definitions extracted from #import <os/log.h>

#if OS_OBJECT_SWIFT3
OS_OBJECT_DECL_SWIFT(os_log);
#elif OS_OBJECT_USE_OBJC
OS_OBJECT_DECL(os_log);
#else
typedef struct os_log_s *os_log_t;
#endif /* OS_OBJECT_USE_OBJC */

extern struct os_log_s _os_log_default;

extern __attribute__((weak)) void _os_log_internal(void *dso, os_log_t log, int type, const char *message, ...);

// In iOS 10 NSLog only shows in device log when debugging from Xcode:
#define NSLog(FORMAT, ...) \
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0")) {\
    void(*ptr_os_log_internal)(void *, __strong os_log_t, int, const char *, ...) = _os_log_internal;\
    if (ptr_os_log_internal != NULL) {\
        _Pragma("clang diagnostic push")\
        _Pragma("clang diagnostic error \"-Wformat\"")\
        _os_log_internal(&__dso_handle, OS_OBJECT_GLOBAL_OBJECT(os_log_t, _os_log_default), 0x00, [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);\
        _Pragma("clang diagnostic pop")\
    } else {\
        NSLog(FORMAT, ##__VA_ARGS__);\
    }\
} else {\
    NSLog(FORMAT, ##__VA_ARGS__);\
}

#endif /* PrefixHeader_pch */
Craig
  • 9,335
  • 2
  • 34
  • 38
Elist
  • 5,313
  • 3
  • 35
  • 73
  • 5
    This is a great answer. Confirming it's not a bug. – d00dle Oct 27 '16 at 20:17
  • 1
    In my opinion this is a poor implementation. Instead of calling these 'if' statements and variable assignment on every call to NSLog, you should better test them once on startup and set their result into global variables that you can refer to in your macro – ishahak Nov 01 '16 at 14:42
  • 3
    @ishahak my purpose was to demonstrate the use of the `os_log` API. You are welcome to edit the code as you see fit. – Elist Nov 01 '16 at 15:55
10

It's an iOS 10 only "feature". Use this instead:

printf("%s", [logString UTF8String]);
Tamás Cseh
  • 3,050
  • 1
  • 20
  • 30
4

You can use this method. Split every 800 chars. Or can be set. NSLOG i think truncate every 1000 chars. If string is less than 800 will use a simple NSLog. This is useful for Json long strings and uses the console. printf uses Xcode debug window not the console.

    -(void) JSLog:(NSString*)logString{

            int stepLog = 800;
            NSInteger strLen = [@([logString length]) integerValue];
            NSInteger countInt = strLen / stepLog;

            if (strLen > stepLog) {
            for (int i=1; i <= countInt; i++) {
                NSString *character = [logString substringWithRange:NSMakeRange((i*stepLog)-stepLog, stepLog)];
                NSLog(@"%@", character);

            }
            NSString *character = [logString substringWithRange:NSMakeRange((countInt*stepLog), strLen-(countInt*stepLog))];
            NSLog(@"%@", character);
            } else {

            NSLog(@"%@", logString);
            }

    }
akim
  • 8,255
  • 3
  • 44
  • 60
Andy Vene
  • 41
  • 2
  • This works great if you need to get over the 1024 character limit in the device console log. Thanks! – AndrewJC Mar 16 '17 at 19:04
2

On iOS 10:

  1. printf() works inside Xcode's console but doesn't work on the device's console log.
  2. NSLog truncates in both places.

What I'm doing for now is splitting my NSLog strings into lines and logging each line individually.

- (void) logString: (NSString *) string
{
    for (NSString *line in [string componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]])
    {
        NSLog(@"%@", line);
    }
}

This works on the console, but isn't easy to read.

Leslie Godwin
  • 2,601
  • 26
  • 18
1

Pretty function and line

#define NSLog(FORMAT, ...) printf("%s:%d %s\n", __PRETTY_FUNCTION__,__LINE__,[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String])

with date

#define NSLog(FORMAT, ...) printf("%s %s:%d %s\n", [[[NSDate date] description] UTF8String],__PRETTY_FUNCTION__,__LINE__,[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String])

improve @xfdai answer

Giang
  • 2,384
  • 2
  • 25
  • 26
0

This doesn't provide a nice output, but prints all necessary information for long logs, even on console.

func Log(_ logString: String?) {
    if logString?.isEmpty ?? false { return }
    NSLog("%@", logString!)
    Log(String(logString!.dropFirst(1024)))
}
Display Name
  • 4,502
  • 2
  • 47
  • 63
0

Here's my Swift workaround for the same problem. I run into this 1000~1024 characters limitation for each interpolated symbol.

Since I mostly run into this when logging json files, I've decided to just split the string into chunks, but not directly. I try to split on the line break to preserve the overall json string between multiple logs.

Another reason behind the provided workaround was to keep the logger.debug(_:) call in the same place where it was, so I won't lose the ability to go to the source code location where it was actually logged.

So here's my solution:

extension String {
    /// Splits the string into chunks to accommodate a specified maximum length,
    /// considering line breaks as the preferred splitting points.
    ///
    /// - Parameters:
    ///   - maxLength: The maximum length of each output chunk. The default value is 1000.
    /// - Returns: An array of `Substring` chunks.
    ///
    /// - Note: The function iterates through the original string and creates chunks of text
    ///         based on the specified maximum length. If a line break character is found
    ///         within the chunk, the split occurs at the line break. If no line break
    ///         character is present, the function tries to split at the last space character
    ///         before the maxLength. If no space is found, the chunk is split at the
    ///         maxLength. The line break character or space character (if used for
    ///         splitting) is dropped from the output.
    ///
    /// - Complexity: The time complexity is O(n), where n is the number of characters
    ///         in the string. The function iterates through the string once to create the
    ///         chunks.
    public func splittedForLogger(maxLength: Int = 1000) -> [Substring] {
        var chunks: [Substring] = []
        var currentIndex = self.startIndex
        
        while currentIndex < self.endIndex {
            let remainingLength = self.distance(from: currentIndex, to: self.endIndex)
            let chunkLength = min(maxLength, remainingLength)
            let nextIndex = self.index(currentIndex, offsetBy: chunkLength)
            let chunk = self[currentIndex..<nextIndex]
            
            if chunkLength == remainingLength {
                /// Last chunk
                chunks.append(chunk)
                break
            }
            
            /// Attempt to find the last line break character within the chunk
            /// If not found, attempt to find the last space character
            /// If neither line break nor space character is found, split at the maxLength
            let splitIndex = chunk.lastIndex { character in
                CharacterSet.newlines.contains(character.unicodeScalars.first ?? .init(0))
            } ?? chunk.lastIndex { character in
                CharacterSet.whitespaces.contains(character.unicodeScalars.first ?? .init(0))
            } ?? chunk.endIndex
            
            let splitChunk = self[currentIndex..<splitIndex]
            chunks.append(splitChunk)
            currentIndex = splitIndex < chunk.endIndex ? self.index(after: splitIndex) : nextIndex
        }
        
        return chunks
    }
    
    @inlinable public func forEachLoggerChunk(
        maxLength: Int = 1000,
        _ body: (Substring) throws -> Void
    ) rethrows {
        try self
            .splittedForLogger(maxLength: maxLength)
            .forEach(body)
    }
}

And now you can use it like this for logging long strings. You just change your

logger.debug("\(someLongString)")

into this

someLongString.forEachLoggerChunk { logger.debug("\($0)") }

Result:

zh.
  • 1,533
  • 1
  • 14
  • 18