NSLog on devices in iOS 10 / Xcode 8 seems to truncate? Why?
Asked Answered
A

8

78

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

enter image description here

Abstracted answered 20/9, 2016 at 2:12 Comment(7)
Just out of curiosity, is the number of "a" characters the same in both images? And exactly how long is the string?Siegel
The number of 'a' is not the same, exactly 1022Abstracted
So it's 1023 out to the first dash? Sounds like only the first 1023 characters are printed.Siegel
I print HTTP response body is incomplete,The above is just an exampleAbstracted
What happens if you log the dictionary value dic[@"key"] instead of the entire dictionary?Jerrylee
try printf instead of NSLog forums.developer.apple.com/message/161367#161367Marybethmaryellen
This caused me so much trouble. Been debugging for days i really thought our server returned a truncated reply. Thanks for the question.Rash
T
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]);
Tachograph answered 21/9, 2016 at 1:55 Comment(3)
Worked for me also :)Marcellusmarcelo
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.Silberman
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])Partridgeberry
B
57

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 */
Bowman answered 27/10, 2016 at 11:35 Comment(3)
This is a great answer. Confirming it's not a bug.Lati
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 macroLavoie
@Lavoie my purpose was to demonstrate the use of the os_log API. You are welcome to edit the code as you see fit.Bowman
D
10

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

printf("%s", [logString UTF8String]);
Durwin answered 20/9, 2016 at 13:35 Comment(2)
What if I need to print an NSDictionary ? This is crazy.Sadowski
@DeepakSharma, the solution provided by xfdai works for NSDictionaries.Tributary
T
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);
            }

    }
Teller answered 6/10, 2016 at 23:16 Comment(1)
This works great if you need to get over the 1024 character limit in the device console log. Thanks!Decompress
H
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.

Heavierthanair answered 30/9, 2016 at 13:33 Comment(0)
E
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

Elute answered 18/5, 2020 at 9:41 Comment(0)
B
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)))
}
Bortz answered 15/4, 2020 at 8:27 Comment(0)
B
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:

Bosson answered 27/7, 2023 at 2:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.