Skip to content

Commit 32d6c68

Browse files
committed
feat(ios): structured app logs that follow relaunches
Devices stream com.apple.os_trace_relay and simulators use log stream, both selected by executable name, so logs survive closing and reopening the app. --log picks the scope: JS output by default, the app's native logs without debug entries with debug, everything with trace or all. Without a new enough ios-device-lib, devices fall back to the syslog relay. Debug builds ask the runtime for chunk markers through logChunkMarkers in the app's package.json, and the CLI stitches split messages back together per process and thread.
1 parent b70757d commit 32d6c68

19 files changed

Lines changed: 876 additions & 322 deletions

‎docs/man_pages/project/testing/run-ios.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Start an emulator with specified device identifier and sdk | `$ ns run ios [--de
3333
* `--emulator` - If set, runs the app in all available and configured ios simulators. It will start a simulator if none are already running.
3434
* `--sdk` - Specifies the target simulator's sdk.
3535
* `--justlaunch` - If set, does not print the application output in the console.
36+
* `--log <Level>` - Also controls how much of the application output is printed. By default only its JavaScript output is shown. `debug` adds everything else the app logs - its native code and the system frameworks it uses, like Xcode's console - and `trace` adds debug level entries as well.
3637
* `--clean` - If set, forces the complete rebuild of the native application.
3738
* `--no-watch` - If set, changes in your code will not be reflected during the execution of this command.
3839
* `--release` - If set, produces a release build by running webpack in production mode and native build in release mode. Otherwise, produces a debug build.

‎lib/common/constants.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,12 @@ export class EmulatorDiscoveryNames {
6262
}
6363

6464
export const DEVICE_LOG_EVENT_NAME = "deviceLogData";
65+
export const DEVICE_LOG_ENTRY_EVENT_NAME = "deviceLogEntry";
6566
export const IOS_LOG_PREDICATE = 'senderImagePath contains "NativeScript"';
66-
export const IOS_APP_CRASH_LOG_REG_EXP = /Fatal JavaScript exception \- application has been terminated/;
67-
export const FAIL_LIVESYNC_LOG_REGEX = /Failed to refresh the application with RefreshRequest./;
67+
export const IOS_APP_CRASH_LOG_REG_EXP =
68+
/Fatal JavaScript exception \- application has been terminated/;
69+
export const FAIL_LIVESYNC_LOG_REGEX =
70+
/Failed to refresh the application with RefreshRequest./;
6871

6972
export const TARGET_FRAMEWORK_IDENTIFIERS = {
7073
Cordova: "Cordova",

‎lib/common/definitions/mobile.d.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,27 @@ declare global {
154154
/**
155155
* Describes log stream options
156156
*/
157+
/**
158+
* A structured unified logging (os_log) entry.
159+
*/
160+
interface IiOSLogEntry {
161+
/**
162+
* Together with the thread, tells apart messages logged at the same time.
163+
*/
164+
pid?: number;
165+
thread?: number;
166+
/**
167+
* Path of the executable of the process that logged the entry.
168+
*/
169+
process: string;
170+
/**
171+
* Path of the binary (executable, framework or library) that logged the entry.
172+
*/
173+
image: string;
174+
level: string;
175+
message: string;
176+
}
177+
157178
interface IiOSLogStreamOptions {
158179
/**
159180
* This is the --predicate option which will be passed to `log stream` command
@@ -213,6 +234,21 @@ declare global {
213234
*/
214235
logData(line: string, platform: string, deviceIdentifier: string): void;
215236

237+
/**
238+
* Logs a structured iOS log entry, when it belongs to the application
239+
* whose logs are shown on the device.
240+
*/
241+
logEntry(
242+
entry: IiOSLogEntry,
243+
platform: string,
244+
deviceIdentifier: string,
245+
): void;
246+
247+
/**
248+
* Returns the project name set for the device, if any.
249+
*/
250+
getProjectNameForDevice(deviceIdentifier: string): string;
251+
216252
/**
217253
* Sets the level of logging that will be used.
218254
* @param {string} level The level of logs - could be INFO or FULL.
@@ -1331,6 +1367,15 @@ declare global {
13311367

13321368
startDeviceLog(deviceIdentifier: string): void;
13331369

1370+
/**
1371+
* Streams structured logs of the process with the given executable name.
1372+
* Returns false when the installed ios-device-lib cannot stream them.
1373+
*/
1374+
startDeviceLogStream(
1375+
deviceIdentifier: string,
1376+
processName: string,
1377+
): boolean;
1378+
13341379
apps(
13351380
deviceIdentifiers: string[],
13361381
errorHandler?: DeviceOperationErrorHandler,

‎lib/common/mobile/device-log-emitter.ts‎

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,71 @@ import { DeviceLogProviderBase } from "./device-log-provider-base";
22
import { DEVICE_LOG_EVENT_NAME } from "../constants";
33
import { injector } from "../yok";
44
import * as _ from "lodash";
5+
import { getIOSLogEntryText, getIOSLogScope } from "./ios/ios-log-entry";
56

67
export class DeviceLogEmitter extends DeviceLogProviderBase {
78
constructor(
89
protected $logFilter: Mobile.ILogFilter,
910
$logger: ILogger,
1011
private $loggingLevels: Mobile.ILoggingLevels,
11-
protected $logSourceMapService: Mobile.ILogSourceMapService
12+
protected $logSourceMapService: Mobile.ILogSourceMapService,
1213
) {
1314
super($logFilter, $logger, $logSourceMapService);
1415
}
1516

1617
public logData(
1718
line: string,
1819
platform: string,
19-
deviceIdentifier: string
20+
deviceIdentifier: string,
2021
): void {
2122
this.setDefaultLogLevelForDevice(deviceIdentifier);
2223

2324
const loggingOptions = this.getDeviceLogOptionsForDevice(
24-
deviceIdentifier
25+
deviceIdentifier,
2526
) || { logLevel: this.$loggingLevels.info, projectDir: null };
2627
let data = this.$logFilter.filterData(platform, line, loggingOptions);
2728
data = this.$logSourceMapService.replaceWithOriginalFileLocations(
2829
platform,
2930
data,
30-
loggingOptions
31+
loggingOptions,
3132
);
3233

34+
this.emitData(data, platform, deviceIdentifier);
35+
}
36+
37+
public logEntry(
38+
entry: Mobile.IiOSLogEntry,
39+
platform: string,
40+
deviceIdentifier: string,
41+
): void {
42+
const loggingOptions = this.getDeviceLogOptionsForDevice(deviceIdentifier);
43+
const text = getIOSLogEntryText(entry, {
44+
processName: loggingOptions && loggingOptions.projectName,
45+
...getIOSLogScope(
46+
this.$logger.getLevel(),
47+
loggingOptions?.logLevel?.toUpperCase() === this.$loggingLevels.full,
48+
),
49+
});
50+
if (text) {
51+
this.stitchIOSLogEntry(entry, text, deviceIdentifier, (message) =>
52+
this.emitData(
53+
this.$logSourceMapService.replaceWithOriginalFileLocations(
54+
platform,
55+
message + "\n",
56+
loggingOptions,
57+
),
58+
platform,
59+
deviceIdentifier,
60+
),
61+
);
62+
}
63+
}
64+
65+
private emitData(
66+
data: string,
67+
platform: string,
68+
deviceIdentifier: string,
69+
): void {
3370
if (data) {
3471
this.emit("data", deviceIdentifier, data);
3572
this.emit(DEVICE_LOG_EVENT_NAME, data, deviceIdentifier, platform);
@@ -42,17 +79,16 @@ export class DeviceLogEmitter extends DeviceLogProviderBase {
4279
deviceIdentifier,
4380
(deviceLogOptions: Mobile.IDeviceLogOptions) =>
4481
deviceLogOptions.logLevel,
45-
logLevel.toUpperCase()
82+
logLevel.toUpperCase(),
4683
);
4784
} else {
4885
this.$logFilter.loggingLevel = logLevel.toUpperCase();
4986

5087
_.keys(this.devicesLogOptions).forEach((deviceId) => {
5188
this.devicesLogOptions[deviceId] =
5289
this.devicesLogOptions[deviceId] || <Mobile.IDeviceLogOptions>{};
53-
this.devicesLogOptions[
54-
deviceId
55-
].logLevel = this.$logFilter.loggingLevel;
90+
this.devicesLogOptions[deviceId].logLevel =
91+
this.$logFilter.loggingLevel;
5692
});
5793
}
5894
}

‎lib/common/mobile/device-log-provider-base.ts‎

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import { EventEmitter } from "events";
22
import { getPropertyName } from "../helpers";
33
import { IDictionary } from "../declarations";
4+
import { IOSLogChunkStitcher, isIOSRuntimeLogEntry } from "./ios/ios-log-entry";
45

56
export abstract class DeviceLogProviderBase
67
extends EventEmitter
78
implements Mobile.IDeviceLogProvider
89
{
910
protected devicesLogOptions: IDictionary<Mobile.IDeviceLogOptions> = {};
11+
private iOSLogChunkStitcher = new IOSLogChunkStitcher();
1012

1113
constructor(
1214
protected $logFilter: Mobile.ILogFilter,
1315
protected $logger: ILogger,
14-
protected $logSourceMapService: Mobile.ILogSourceMapService
16+
protected $logSourceMapService: Mobile.ILogSourceMapService,
1517
) {
1618
super();
1719
}
1820

1921
public async setSourceFileLocation(
20-
pathToOriginalFile: string
22+
pathToOriginalFile: string,
2123
): Promise<void> {
2224
try {
2325
await this.$logSourceMapService.setSourceMapConsumerForFile(
24-
pathToOriginalFile
26+
pathToOriginalFile,
2527
);
2628
} catch (err) {
2729
this.$logger.trace("Error while trying to set source map file", err);
@@ -31,23 +33,47 @@ export abstract class DeviceLogProviderBase
3133
public abstract logData(
3234
lineText: string,
3335
platform: string,
34-
deviceIdentifier: string
36+
deviceIdentifier: string,
37+
): void;
38+
39+
public abstract logEntry(
40+
entry: Mobile.IiOSLogEntry,
41+
platform: string,
42+
deviceIdentifier: string,
3543
): void;
3644

3745
public abstract setLogLevel(
3846
logLevel: string,
39-
deviceIdentifier?: string
47+
deviceIdentifier?: string,
4048
): void;
4149

50+
protected stitchIOSLogEntry(
51+
entry: Mobile.IiOSLogEntry,
52+
text: string,
53+
deviceIdentifier: string,
54+
emit: (text: string) => void,
55+
): void {
56+
if (!isIOSRuntimeLogEntry(entry)) {
57+
emit(text);
58+
return;
59+
}
60+
61+
this.iOSLogChunkStitcher.add(
62+
`${deviceIdentifier}:${entry.pid}:${entry.thread}`,
63+
text,
64+
emit,
65+
);
66+
}
67+
4268
public setApplicationPidForDevice(
4369
deviceIdentifier: string,
44-
pid: string
70+
pid: string,
4571
): void {
4672
this.setDeviceLogOptionsProperty(
4773
deviceIdentifier,
4874
(deviceLogOptions: Mobile.IDeviceLogOptions) =>
4975
deviceLogOptions.applicationPid,
50-
pid
76+
pid,
5177
);
5278
}
5379

@@ -56,31 +82,31 @@ export abstract class DeviceLogProviderBase
5682
deviceIdentifier,
5783
(deviceLogOptions: Mobile.IDeviceLogOptions) =>
5884
deviceLogOptions.applicationId,
59-
appId
85+
appId,
6086
);
6187
}
6288

6389
public setProjectNameForDevice(
6490
deviceIdentifier: string,
65-
projectName: string
91+
projectName: string,
6692
): void {
6793
this.setDeviceLogOptionsProperty(
6894
deviceIdentifier,
6995
(deviceLogOptions: Mobile.IDeviceLogOptions) =>
7096
deviceLogOptions.projectName,
71-
projectName
97+
projectName,
7298
);
7399
}
74100

75101
public setProjectDirForDevice(
76102
deviceIdentifier: string,
77-
projectDir: string
103+
projectDir: string,
78104
): void {
79105
this.setDeviceLogOptionsProperty(
80106
deviceIdentifier,
81107
(deviceLogOptions: Mobile.IDeviceLogOptions) =>
82108
deviceLogOptions.projectDir,
83-
projectDir
109+
projectDir,
84110
);
85111
}
86112

@@ -99,6 +125,13 @@ export abstract class DeviceLogProviderBase
99125
);
100126
}
101127

128+
public getProjectNameForDevice(deviceIdentifier: string): string {
129+
return (
130+
this.devicesLogOptions[deviceIdentifier] &&
131+
this.devicesLogOptions[deviceIdentifier].projectName
132+
);
133+
}
134+
102135
getApplicationIdForDevice(deviceIdentifier: string) {
103136
return (
104137
this.devicesLogOptions[deviceIdentifier] &&
@@ -107,7 +140,7 @@ export abstract class DeviceLogProviderBase
107140
}
108141

109142
protected getDeviceLogOptionsForDevice(
110-
deviceIdentifier: string
143+
deviceIdentifier: string,
111144
): Mobile.IDeviceLogOptions {
112145
const loggingOptions = this.devicesLogOptions[deviceIdentifier];
113146
if (!loggingOptions) {
@@ -120,7 +153,7 @@ export abstract class DeviceLogProviderBase
120153
protected setDeviceLogOptionsProperty(
121154
deviceIdentifier: string,
122155
propNameFunction: Function,
123-
propertyValue: string | boolean
156+
propertyValue: string | boolean,
124157
): void {
125158
const propertyName = getPropertyName(propNameFunction);
126159

0 commit comments

Comments
 (0)