Skip to content

Commit 6e0eacf

Browse files
authored
get metadata from client log (#950)
1 parent 2756d12 commit 6e0eacf

File tree

3 files changed

+127
-2
lines changed

3 files changed

+127
-2
lines changed

src/daemon/clientLog/logWatcher.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
import * as vscode from "vscode";
4+
import { sendInfo } from "vscode-extension-telemetry-wrapper";
5+
import { LSDaemon } from "../daemon";
6+
7+
export class ClientLogWatcher {
8+
private context: vscode.ExtensionContext;
9+
private javaExtensionRoot: vscode.Uri | undefined;
10+
private logProcessedTimestamp: number = Date.now();
11+
12+
constructor(daemon: LSDaemon) {
13+
this.context = daemon.context;
14+
if (this.context.storageUri) {
15+
this.javaExtensionRoot = vscode.Uri.joinPath(this.context.storageUri, "..", "redhat.java");
16+
}
17+
}
18+
19+
public async collectStartupInfo() {
20+
const logs = await this.getLogs();
21+
if (logs) {
22+
const info: any = {};
23+
24+
const jdkLog = logs.find(log => log.message.startsWith("Use the JDK from"));
25+
info.defaultProjectJdk = jdkLog?.message.replace("Use the JDK from '", "").replace("' as the initial default project JDK.", "");
26+
27+
const startupLog = logs.find(log => log.message.startsWith("Starting Java server with:") && log.message.endsWith("jdt_ws") /* limit to standard server */);
28+
if (startupLog) {
29+
info.xmx = startupLog.message.match(/-Xmx[0-9kmgKMG]+/g)?.[0];
30+
info.xms = startupLog.message.match(/-Xms[0-9kmgKMG]+/g)?.[0];
31+
info.lombok = startupLog.message.includes("lombok.jar") ? "true" : undefined;
32+
info.workspaceType = startupLog.message.match(/-XX:HeapDumpPath=.*(vscodesws)/) ? "vscodesws": "folder";
33+
}
34+
35+
const errorLog = logs.find(log => log.level === "error");
36+
info.error = errorLog ? "true" : undefined;
37+
38+
const missingJar = "Error opening zip file or JAR manifest missing"; // lombok especially
39+
if (logs.find(log => log.message.startsWith(missingJar))) {
40+
info.error = missingJar;
41+
}
42+
43+
const crashLog = logs.find(log => log.message.startsWith("The Language Support for Java server crashed and will restart."));
44+
info.crash = crashLog ? "true" : undefined;
45+
46+
sendInfo("", {
47+
name: "client-log-startup-metadata",
48+
...info
49+
});
50+
}
51+
}
52+
53+
public async getLogs() {
54+
const rawBytes = await this.readLatestLogFile();
55+
if (rawBytes) {
56+
const content = rawBytes.toString();
57+
const entries = parse(content);
58+
return entries.filter(elem => Date.parse(elem["timestamp"]) > this.logProcessedTimestamp);
59+
} else {
60+
return undefined;
61+
}
62+
}
63+
64+
private async readLatestLogFile() {
65+
if (this.javaExtensionRoot) {
66+
const files = await vscode.workspace.fs.readDirectory(this.javaExtensionRoot);
67+
const logFiles = files.filter(elem => elem[0].startsWith("client.log")).sort((a, b) => compare_file(a[0], b[0]));
68+
if (logFiles.length > 0) {
69+
const latestLogFile = logFiles[logFiles.length - 1][0];
70+
const uri = vscode.Uri.joinPath(this.javaExtensionRoot, latestLogFile);
71+
return await vscode.workspace.fs.readFile(uri);
72+
}
73+
}
74+
return undefined;
75+
}
76+
}
77+
/**
78+
* filename: client.log.yyyy-mm-dd.r
79+
*/
80+
function compare_file(a: string, b: string) {
81+
const dateA = a.slice(11, 21), dateB = b.slice(11, 21);
82+
if (dateA === dateB) {
83+
if (a.length > 22 && b.length > 22) {
84+
const extA = a.slice(22), extB = b.slice(22);
85+
return parseInt(extA) - parseInt(extB);
86+
} else {
87+
return a.length - b.length;
88+
}
89+
} else {
90+
return dateA < dateB ? -1 : 1;
91+
}
92+
}
93+
94+
function parse(rawLog: string) {
95+
const SEP = /\r?\n/;
96+
const START = "{";
97+
const END= "}";
98+
99+
const ret = [];
100+
101+
let current: { [key: string]: string } | undefined = undefined;
102+
for(const line of rawLog.split(SEP)) {
103+
if (line === START) {
104+
current = {};
105+
} else if (line === END) {
106+
if (current !== undefined) {
107+
ret.push(current);
108+
current = undefined;
109+
}
110+
} else {
111+
if (current !== undefined) {
112+
const m = line.match(/^\s*(.*):\s['"](.*?)['"],?$/);
113+
if (m) {
114+
current[m[1]] = m[2];
115+
}
116+
}
117+
}
118+
}
119+
return ret;
120+
}

src/daemon/daemon.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@
22
// Licensed under the MIT license.
33

44
import * as vscode from "vscode";
5+
import { ClientLogWatcher } from "./clientLog/logWatcher";
56
import { ProcessWatcher } from "./processWatcher";
67
import { LogWatcher } from "./serverLog/logWatcher";
78

8-
99
export class LSDaemon {
1010

1111
public logWatcher: LogWatcher;
1212
public processWatcher: ProcessWatcher;
13+
public clientLogWatcher: ClientLogWatcher;
1314

1415
constructor(public context: vscode.ExtensionContext) {
1516
this.processWatcher = new ProcessWatcher(this);
1617
this.logWatcher = new LogWatcher(this);
18+
this.clientLogWatcher = new ClientLogWatcher(this)
1719
}
1820

1921
public async initialize() {
2022
await this.logWatcher.start();
23+
setTimeout(() => {
24+
this.clientLogWatcher.collectStartupInfo();
25+
}, 10 * 1000); // wait a while when JDTLS has been launched
2126
}
2227

2328

src/daemon/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const delay = promisify(setTimeout);
1111
let daemon: LSDaemon;
1212
export async function initDaemon(context: vscode.ExtensionContext) {
1313
daemon = new LSDaemon(context);
14-
await daemon.initialize()
14+
await daemon.initialize();
1515

1616
const activated = await checkJavaExtActivated(context);
1717
if (activated) {

0 commit comments

Comments
 (0)