querysub 0.312.0 → 0.313.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +1 -1
- package/costsBenefits.txt +4 -1
- package/package.json +3 -2
- package/spec.txt +23 -18
- package/src/-0-hooks/hooks.ts +1 -1
- package/src/-a-archives/archives.ts +16 -3
- package/src/-a-archives/archivesBackBlaze.ts +51 -3
- package/src/-a-archives/archivesLimitedCache.ts +175 -0
- package/src/-a-archives/archivesPrivateFileSystem.ts +299 -0
- package/src/-a-auth/certs.ts +58 -31
- package/src/-b-authorities/cdnAuthority.ts +2 -2
- package/src/-b-authorities/dnsAuthority.ts +3 -2
- package/src/-c-identity/IdentityController.ts +3 -2
- package/src/-d-trust/NetworkTrust2.ts +17 -19
- package/src/-e-certs/EdgeCertController.ts +3 -4
- package/src/-e-certs/certAuthority.ts +1 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +9 -7
- package/src/-g-core-values/NodeCapabilities.ts +6 -1
- package/src/0-path-value-core/NodePathAuthorities.ts +1 -1
- package/src/0-path-value-core/PathValueCommitter.ts +3 -3
- package/src/0-path-value-core/PathValueController.ts +3 -3
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +15 -37
- package/src/0-path-value-core/pathValueCore.ts +4 -3
- package/src/3-path-functions/PathFunctionRunner.ts +2 -2
- package/src/4-dom/qreact.tsx +4 -3
- package/src/4-querysub/Querysub.ts +2 -2
- package/src/4-querysub/QuerysubController.ts +2 -2
- package/src/5-diagnostics/GenericFormat.tsx +1 -0
- package/src/5-diagnostics/Table.tsx +3 -0
- package/src/5-diagnostics/diskValueAudit.ts +2 -1
- package/src/5-diagnostics/nodeMetadata.ts +0 -1
- package/src/deployManager/components/MachineDetailPage.tsx +9 -1
- package/src/deployManager/components/ServiceDetailPage.tsx +10 -1
- package/src/diagnostics/NodeViewer.tsx +3 -4
- package/src/diagnostics/logs/FastArchiveAppendable.ts +748 -0
- package/src/diagnostics/logs/FastArchiveController.ts +524 -0
- package/src/diagnostics/logs/FastArchiveViewer.tsx +863 -0
- package/src/diagnostics/logs/LogViewer2.tsx +349 -0
- package/src/diagnostics/logs/TimeRangeSelector.tsx +94 -0
- package/src/diagnostics/logs/diskLogger.ts +135 -305
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +6 -29
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +577 -0
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +225 -0
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +207 -0
- package/src/diagnostics/logs/importLogsEntry.ts +38 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +7 -17
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +0 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +151 -0
- package/src/diagnostics/managementPages.tsx +7 -16
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +0 -1
- package/src/diagnostics/periodic.ts +5 -0
- package/src/diagnostics/watchdog.ts +2 -2
- package/src/functional/SocketChannel.ts +67 -0
- package/src/library-components/Input.tsx +1 -1
- package/src/library-components/InputLabel.tsx +5 -2
- package/src/misc.ts +111 -0
- package/src/src.d.ts +34 -1
- package/src/user-implementation/userData.ts +4 -3
- package/test.ts +13 -0
- package/testEntry2.ts +29 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +0 -535
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +0 -274
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +0 -308
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +0 -84
- package/src/diagnostics/errorLogs/LogNotify.tsx +0 -101
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +0 -723
- package/src/diagnostics/errorLogs/LogViewer.tsx +0 -757
- package/src/diagnostics/errorLogs/logFiltering.tsx +0 -149
- package/src/diagnostics/logs/DiskLoggerPage.tsx +0 -613
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { qreact } from "../../../4-dom/qreact";
|
|
3
|
+
import { NOT_AN_ERROR_EXPIRE_TIME, RecentErrorsController, SuppressionEntry, SuppressionListController, getSuppressEntryChecker } from "./ErrorNotificationController";
|
|
4
|
+
import { t } from "../../../2-proxy/schema2";
|
|
5
|
+
import { css } from "../../../4-dom/css";
|
|
6
|
+
import { InputLabel } from "../../../library-components/InputLabel";
|
|
7
|
+
import { Button } from "../../../library-components/Button";
|
|
8
|
+
import { Querysub } from "../../../4-querysub/QuerysubController";
|
|
9
|
+
import { nextId, sort, timeInDay } from "socket-function/src/misc";
|
|
10
|
+
import { formatNumber, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
11
|
+
import { formatDateJSX } from "../../../misc/formatJSX";
|
|
12
|
+
import { LogDatum } from "../diskLogger";
|
|
13
|
+
|
|
14
|
+
export class ErrorSuppressionUI extends qreact.Component<{
|
|
15
|
+
dataSeqNum: number;
|
|
16
|
+
suppressionCounts: Map<string, number>;
|
|
17
|
+
expiredSuppressionCounts: Map<string, number>;
|
|
18
|
+
datums: LogDatum[];
|
|
19
|
+
rerunFilters: () => void;
|
|
20
|
+
}> {
|
|
21
|
+
state = t.state({
|
|
22
|
+
matchedInput: t.string(""),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
private calculatePreviewMatchCount(pattern: string): number {
|
|
26
|
+
if (!pattern.trim()) return 0;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
let entry: SuppressionEntry = {
|
|
30
|
+
key: "preview",
|
|
31
|
+
match: pattern,
|
|
32
|
+
comment: "",
|
|
33
|
+
lastUpdateTime: Date.now(),
|
|
34
|
+
expiresAt: Date.now() + timeInDay,
|
|
35
|
+
};
|
|
36
|
+
let checker = getSuppressEntryChecker(entry);
|
|
37
|
+
|
|
38
|
+
let matchCount = 0;
|
|
39
|
+
for (let datum of this.props.datums.slice(0, 1000)) {
|
|
40
|
+
let datumStr = JSON.stringify(datum);
|
|
41
|
+
let buffer = Buffer.from(datumStr);
|
|
42
|
+
if (checker.fnc(buffer, 0, buffer.length)) {
|
|
43
|
+
matchCount++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return matchCount;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public render() {
|
|
53
|
+
this.props.dataSeqNum;
|
|
54
|
+
const controller = SuppressionListController(SocketFunction.browserNodeId());
|
|
55
|
+
const entries = (controller.getSuppressionList() || []);
|
|
56
|
+
sort(entries, x => -x.lastUpdateTime);
|
|
57
|
+
sort(entries, x => -(this.props.suppressionCounts.get(x.key) ? 1 : 0));
|
|
58
|
+
sort(entries, x => -(this.props.expiredSuppressionCounts.get(x.key) ? 1 : 0));
|
|
59
|
+
|
|
60
|
+
const previewMatchCount = this.calculatePreviewMatchCount(this.state.matchedInput);
|
|
61
|
+
|
|
62
|
+
return <div className={css.vbox(16).pad2(16).fillWidth.bord2(0, 0, 50, 5).hsl(0, 0, 80)}>
|
|
63
|
+
<h2 className={css.fontSize(18).marginTop(0)}>Error Suppression List</h2>
|
|
64
|
+
|
|
65
|
+
<div className={css.hbox(8).fillWidth}>
|
|
66
|
+
<InputLabel
|
|
67
|
+
label="Add Match"
|
|
68
|
+
value={this.state.matchedInput}
|
|
69
|
+
hot
|
|
70
|
+
onChangeValue={(value: string) => this.state.matchedInput = value}
|
|
71
|
+
onKeyDown={e => {
|
|
72
|
+
if (e.key === "Enter") {
|
|
73
|
+
let value = e.currentTarget.value;
|
|
74
|
+
e.currentTarget.value = "";
|
|
75
|
+
void Querysub.onCommitFinished(async () => {
|
|
76
|
+
await controller.setSuppressionEntry.promise({
|
|
77
|
+
key: nextId(),
|
|
78
|
+
match: value,
|
|
79
|
+
comment: "",
|
|
80
|
+
lastUpdateTime: Date.now(),
|
|
81
|
+
expiresAt: Date.now() + timeInDay,
|
|
82
|
+
});
|
|
83
|
+
if (!e.shiftKey) {
|
|
84
|
+
Querysub.commit(() => {
|
|
85
|
+
this.props.rerunFilters();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
fillWidth
|
|
92
|
+
placeholder="* is supported as a wildcard (enter to submit and refresh, shift+enter to submit and do not refresh)"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
{this.state.matchedInput.trim() && <span className={css.opacity(0.6).fontSize(12).flexShrink0}>
|
|
96
|
+
matches {formatNumber(previewMatchCount)} (in top 1000)
|
|
97
|
+
</span>}
|
|
98
|
+
<Button onClick={() => {
|
|
99
|
+
let value = this.state.matchedInput;
|
|
100
|
+
this.state.matchedInput = "";
|
|
101
|
+
void Querysub.onCommitFinished(async () => {
|
|
102
|
+
await controller.setSuppressionEntry.promise({
|
|
103
|
+
key: nextId(),
|
|
104
|
+
match: value,
|
|
105
|
+
comment: "",
|
|
106
|
+
lastUpdateTime: Date.now(),
|
|
107
|
+
expiresAt: NOT_AN_ERROR_EXPIRE_TIME,
|
|
108
|
+
});
|
|
109
|
+
Querysub.commit(() => {
|
|
110
|
+
this.props.rerunFilters();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}}>
|
|
114
|
+
Not an error
|
|
115
|
+
</Button>
|
|
116
|
+
<Button
|
|
117
|
+
onClick={() => {
|
|
118
|
+
let value = this.state.matchedInput;
|
|
119
|
+
this.state.matchedInput = "";
|
|
120
|
+
void Querysub.onCommitFinished(async () => {
|
|
121
|
+
await controller.setSuppressionEntry.promise({
|
|
122
|
+
key: nextId(),
|
|
123
|
+
match: value,
|
|
124
|
+
comment: "",
|
|
125
|
+
lastUpdateTime: Date.now(),
|
|
126
|
+
expiresAt: Date.now() + timeInDay,
|
|
127
|
+
});
|
|
128
|
+
Querysub.commit(() => {
|
|
129
|
+
this.props.rerunFilters();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}}
|
|
133
|
+
title="Fixed, or will fix within a day"
|
|
134
|
+
>
|
|
135
|
+
Fixed
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className={css.vbox(8).fillWidth.overflowAuto.maxHeight("30vh")}>
|
|
140
|
+
{entries.map((entry) => {
|
|
141
|
+
const updateEntry = (changes: Partial<SuppressionEntry>) => {
|
|
142
|
+
let newEntry = { ...entry, ...changes };
|
|
143
|
+
void Querysub.onCommitFinished(async () => {
|
|
144
|
+
await controller.setSuppressionEntry.promise(newEntry);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
let count = this.props.suppressionCounts.get(entry.key) || 0;
|
|
148
|
+
let expiredCount = this.props.expiredSuppressionCounts.get(entry.key) || 0;
|
|
149
|
+
return <div
|
|
150
|
+
key={entry.match}
|
|
151
|
+
className={
|
|
152
|
+
css.hbox(8).pad2(12).bord2(0, 0, 10).fillWidth
|
|
153
|
+
//+ (entry.expiresAt < Date.now() && expiredCount > 0 && css.opacity(0.5))
|
|
154
|
+
+ ((count === 0 && expiredCount === 0) && css.opacity(0.6))
|
|
155
|
+
+ (
|
|
156
|
+
count > 0 && entry.expiresAt !== NOT_AN_ERROR_EXPIRE_TIME && css.hsla(0, 50, 50, 0.5)
|
|
157
|
+
|| css.hsla(0, 0, 0, 0.1)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
<span className={css.width(100).hbox(2) + (expiredCount === 0 && count === 0 && css.opacity(0.8))}>
|
|
162
|
+
({formatNumber(count)})
|
|
163
|
+
{
|
|
164
|
+
expiredCount > 0 && <span className={css.hsl(0, 50, 50).pad2(4, 2).colorhsl(0, 50, 95)}>
|
|
165
|
+
+ {formatNumber(expiredCount)} EXPIRED
|
|
166
|
+
</span>
|
|
167
|
+
}
|
|
168
|
+
</span>
|
|
169
|
+
<InputLabel
|
|
170
|
+
label="Match Pattern"
|
|
171
|
+
value={entry.match}
|
|
172
|
+
onChangeValue={value => updateEntry({ match: value })}
|
|
173
|
+
fillWidth={0.5}
|
|
174
|
+
/>
|
|
175
|
+
<InputLabel
|
|
176
|
+
label="Comment"
|
|
177
|
+
value={entry.comment}
|
|
178
|
+
onChangeValue={value => updateEntry({ comment: value })}
|
|
179
|
+
fillWidth
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<Button
|
|
183
|
+
onClick={() => updateEntry({ expiresAt: Date.now() + timeInDay })}
|
|
184
|
+
title="Fixed, or will fix within a day"
|
|
185
|
+
>
|
|
186
|
+
Fixed
|
|
187
|
+
</Button>
|
|
188
|
+
<Button onClick={() => updateEntry({ expiresAt: Date.now() + timeInDay * 7 })}
|
|
189
|
+
title="Ignore for a week"
|
|
190
|
+
>
|
|
191
|
+
Ignore (for a week)
|
|
192
|
+
</Button>
|
|
193
|
+
<Button onClick={() => updateEntry({ expiresAt: NOT_AN_ERROR_EXPIRE_TIME })}>
|
|
194
|
+
Not an error
|
|
195
|
+
</Button>
|
|
196
|
+
<Button
|
|
197
|
+
onClick={() => updateEntry({ expiresAt: Date.now() - timeInDay * 7 })}
|
|
198
|
+
title="Unignore"
|
|
199
|
+
>
|
|
200
|
+
Unignore
|
|
201
|
+
</Button>
|
|
202
|
+
{entry.expiresAt === NOT_AN_ERROR_EXPIRE_TIME && <span>
|
|
203
|
+
Not an error
|
|
204
|
+
</span>}
|
|
205
|
+
{entry.expiresAt < Date.now() && <span>
|
|
206
|
+
Expired
|
|
207
|
+
</span>}
|
|
208
|
+
{entry.expiresAt > Date.now() && entry.expiresAt !== NOT_AN_ERROR_EXPIRE_TIME && <span>
|
|
209
|
+
Expires in {formatDateJSX(Date.now() * 2 - entry.expiresAt)}
|
|
210
|
+
</span>}
|
|
211
|
+
<span className={css.opacity(0.5)}>
|
|
212
|
+
(Last Updated: {formatDateJSX(entry.lastUpdateTime)})
|
|
213
|
+
</span>
|
|
214
|
+
<Button
|
|
215
|
+
onClick={() => controller.removeSuppressionEntry.promise(entry.key)}
|
|
216
|
+
hue={120}
|
|
217
|
+
>
|
|
218
|
+
Remove
|
|
219
|
+
</Button>
|
|
220
|
+
</div>;
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
</div>;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { qreact } from "../../../4-dom/qreact";
|
|
3
|
+
import { css } from "../../../4-dom/css";
|
|
4
|
+
import { isCurrentUserSuperUser } from "../../../user-implementation/userData";
|
|
5
|
+
import { RecentErrorsController, SuppressionListController, watchRecentErrors, MAX_RECENT_ERRORS, NOT_AN_ERROR_EXPIRE_TIME, SuppressionEntry } from "./ErrorNotificationController";
|
|
6
|
+
import { t } from "../../../2-proxy/schema2";
|
|
7
|
+
import { InputLabel } from "../../../library-components/InputLabel";
|
|
8
|
+
import { Button } from "../../../library-components/Button";
|
|
9
|
+
import { ATag } from "../../../library-components/ATag";
|
|
10
|
+
import { managementPageURL, showingManagementURL } from "../../managementPages";
|
|
11
|
+
import { errorNotifyToggleURL } from "../LogViewer2";
|
|
12
|
+
import { Querysub } from "../../../4-querysub/QuerysubController";
|
|
13
|
+
import { nextId, timeInDay } from "socket-function/src/misc";
|
|
14
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
15
|
+
import { Icon } from "../../../library-components/icons";
|
|
16
|
+
|
|
17
|
+
export class ErrorWarning extends qreact.Component {
|
|
18
|
+
state = t.state({
|
|
19
|
+
suppressionInput: t.string(""),
|
|
20
|
+
reloading: t.boolean(false),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
public render() {
|
|
24
|
+
if (!isCurrentUserSuperUser()) return undefined;
|
|
25
|
+
watchRecentErrors();
|
|
26
|
+
|
|
27
|
+
let recentController = RecentErrorsController(SocketFunction.browserNodeId());
|
|
28
|
+
let suppressionController = SuppressionListController(SocketFunction.browserNodeId());
|
|
29
|
+
|
|
30
|
+
let recentErrors = recentController.getRecentErrors();
|
|
31
|
+
|
|
32
|
+
const refreshButton =
|
|
33
|
+
<Button
|
|
34
|
+
onClick={() => {
|
|
35
|
+
this.state.reloading = true;
|
|
36
|
+
void Querysub.onCommitFinished(async () => {
|
|
37
|
+
try {
|
|
38
|
+
await recentController.rescanAllErrorsNow.promise();
|
|
39
|
+
} finally {
|
|
40
|
+
Querysub.commit(() => {
|
|
41
|
+
this.state.reloading = false;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}}
|
|
46
|
+
flavor="noui"
|
|
47
|
+
className={
|
|
48
|
+
css.button.hsla(0, 0, 50, 1).pad2(4, 1).borderRadius(2)
|
|
49
|
+
+ (this.state.reloading && css.animation("ErrorWarning-rotate 1s linear infinite"))
|
|
50
|
+
}
|
|
51
|
+
>
|
|
52
|
+
⟳
|
|
53
|
+
<style>
|
|
54
|
+
{
|
|
55
|
+
`@keyframes ErrorWarning-rotate {
|
|
56
|
+
from {
|
|
57
|
+
transform: rotate(0deg);
|
|
58
|
+
}
|
|
59
|
+
to {
|
|
60
|
+
transform: rotate(360deg);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`}
|
|
64
|
+
</style>
|
|
65
|
+
</Button>;
|
|
66
|
+
|
|
67
|
+
if (!recentErrors || recentErrors.length === 0) {
|
|
68
|
+
return <span className={css.hbox(8)}>
|
|
69
|
+
<ATag target="_blank" values={[showingManagementURL.getOverride(true), managementPageURL.getOverride("LogViewer2"), errorNotifyToggleURL.getOverride(true)]}>
|
|
70
|
+
No Errors
|
|
71
|
+
</ATag>
|
|
72
|
+
{refreshButton}
|
|
73
|
+
</span>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Count unique files
|
|
77
|
+
let fileSet = new Set<string>();
|
|
78
|
+
for (let error of recentErrors) {
|
|
79
|
+
let file = String(error.__FILE__) || "";
|
|
80
|
+
if (file) {
|
|
81
|
+
fileSet.add(file);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let fileCount = fileSet.size;
|
|
85
|
+
let fileCountText = `${formatNumber(fileCount)} files matched`;
|
|
86
|
+
if (recentErrors.length >= MAX_RECENT_ERRORS) {
|
|
87
|
+
fileCountText = "> " + fileCountText;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const addSuppressionEntry = async (match: string, expiresAt: number) => {
|
|
91
|
+
await suppressionController.setSuppressionEntry.promise({
|
|
92
|
+
key: nextId(),
|
|
93
|
+
match: match,
|
|
94
|
+
comment: "",
|
|
95
|
+
lastUpdateTime: Date.now(),
|
|
96
|
+
expiresAt: expiresAt,
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let suppressionList = suppressionController.getSuppressionList() || [];
|
|
101
|
+
let topExpired: SuppressionEntry | undefined = undefined;
|
|
102
|
+
if (recentErrors[0].__matchedOutdatedSuppressionKey) {
|
|
103
|
+
let key = recentErrors[0].__matchedOutdatedSuppressionKey;
|
|
104
|
+
topExpired = suppressionList.find(x => x.key === key);
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className={css.vbox(8).fillWidth}>
|
|
110
|
+
<div className={css.hbox(8).wrap}>
|
|
111
|
+
<div className={css}>
|
|
112
|
+
⚠️ {fileCountText} files with errors
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<ATag target="_blank" values={[showingManagementURL.getOverride(true), managementPageURL.getOverride("LogViewer2"), errorNotifyToggleURL.getOverride(true)]}>
|
|
116
|
+
View Logs
|
|
117
|
+
</ATag>
|
|
118
|
+
{refreshButton}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{topExpired &&
|
|
122
|
+
<div className={css.hbox(8)}>
|
|
123
|
+
<Button onClick={() => {
|
|
124
|
+
void Querysub.onCommitFinished(async () => {
|
|
125
|
+
await suppressionController.setSuppressionEntry.promise({
|
|
126
|
+
...topExpired!,
|
|
127
|
+
expiresAt: Date.now() + timeInDay,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}}>
|
|
131
|
+
Ignore Again
|
|
132
|
+
</Button>
|
|
133
|
+
<div>
|
|
134
|
+
Match Pattern =
|
|
135
|
+
</div>
|
|
136
|
+
<div className={css.hsl(200, 40, 40).pad2(4, 2).colorhsl(0, 0, 95)}>
|
|
137
|
+
{topExpired.match}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
}
|
|
141
|
+
<div className={css.hbox(8).hsl(0, 50, 50).pad2(4, 2).colorhsl(0, 50, 95)}>
|
|
142
|
+
{recentErrors[0].param0} ({recentErrors[0].__NAME__})
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className={css.hbox(8).fillWidth}>
|
|
146
|
+
<InputLabel
|
|
147
|
+
value={this.state.suppressionInput}
|
|
148
|
+
noEnterKeyBlur
|
|
149
|
+
onChangeValue={(value: string) => this.state.suppressionInput = value}
|
|
150
|
+
onKeyDown={e => {
|
|
151
|
+
if (e.key === "Enter") {
|
|
152
|
+
let value = e.currentTarget.value;
|
|
153
|
+
if (value.trim()) {
|
|
154
|
+
e.currentTarget.value = "";
|
|
155
|
+
void Querysub.onCommitFinished(async () => {
|
|
156
|
+
await addSuppressionEntry(value, Date.now() + timeInDay);
|
|
157
|
+
Querysub.commit(() => {
|
|
158
|
+
this.state.suppressionInput = "";
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
placeholder="Quick suppress pattern (press enter to add)"
|
|
165
|
+
fillWidth
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
<Button
|
|
170
|
+
onClick={() => {
|
|
171
|
+
let value = this.state.suppressionInput;
|
|
172
|
+
if (value.trim()) {
|
|
173
|
+
void Querysub.onCommitFinished(async () => {
|
|
174
|
+
await addSuppressionEntry(value, Date.now() + timeInDay);
|
|
175
|
+
Querysub.commit(() => {
|
|
176
|
+
this.state.suppressionInput = "";
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}}
|
|
181
|
+
className={css.fontSize(12).pad2(4, 6)}
|
|
182
|
+
disabled={!this.state.suppressionInput.trim()}
|
|
183
|
+
>
|
|
184
|
+
Fixed
|
|
185
|
+
</Button>
|
|
186
|
+
<Button
|
|
187
|
+
onClick={() => {
|
|
188
|
+
let value = this.state.suppressionInput;
|
|
189
|
+
if (value.trim()) {
|
|
190
|
+
void Querysub.onCommitFinished(async () => {
|
|
191
|
+
await addSuppressionEntry(value, NOT_AN_ERROR_EXPIRE_TIME);
|
|
192
|
+
Querysub.commit(() => {
|
|
193
|
+
this.state.suppressionInput = "";
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}}
|
|
198
|
+
className={css.fontSize(12).pad2(4, 6)}
|
|
199
|
+
disabled={!this.state.suppressionInput.trim()}
|
|
200
|
+
>
|
|
201
|
+
Not a bug
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Querysub } from "../../4-querysub/Querysub";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { Zip } from "../../zip";
|
|
4
|
+
import { getLoggers } from "./diskLogger";
|
|
5
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
6
|
+
import { shutdown } from "../periodic";
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
let { logLogs } = getLoggers()!;
|
|
10
|
+
//let root = "X:/root/machine-services/cyoa-1-dply/git/database-storage/disklogs";
|
|
11
|
+
/*
|
|
12
|
+
let root = "D:/repos/qs-cyoa/database-storage/disklogs/";
|
|
13
|
+
let files = await fs.promises.readdir(root);
|
|
14
|
+
for (let fileName of files) {
|
|
15
|
+
let path = root + fileName;
|
|
16
|
+
if (!path.endsWith(".log")) continue;
|
|
17
|
+
let contents = await fs.promises.readFile(path);
|
|
18
|
+
let lines = contents.toString("utf8").split("\n");
|
|
19
|
+
for (let line of lines) {
|
|
20
|
+
if (!line) continue;
|
|
21
|
+
try {
|
|
22
|
+
let log = JSON.parse(line);
|
|
23
|
+
logLogs.append(log);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
require("debugbreak")(2);
|
|
26
|
+
debugger;
|
|
27
|
+
console.error(e, line);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.log(`Flushing ${path}, line count: ${formatNumber(lines.length)}, ${formatNumber(contents.length)}B`);
|
|
31
|
+
let time = Number(fileName.split("-")[0]) || Date.now();
|
|
32
|
+
await logLogs.flushNow(time);
|
|
33
|
+
}
|
|
34
|
+
*/
|
|
35
|
+
await logLogs.moveLogsToBackblaze();
|
|
36
|
+
//await shutdown();
|
|
37
|
+
}
|
|
38
|
+
main().catch(console.error).finally(() => process.exit());
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { css, isNode } from "typesafecss";
|
|
2
2
|
import { compileTransformBefore } from "typenode";
|
|
3
|
+
|
|
3
4
|
// IMPORTANT! Don't add any imports BEFORE we add compileTransform
|
|
4
5
|
if (isNode()) {
|
|
5
|
-
// NOTE: While mapping line numbers would be nice, just adding the file is
|
|
6
|
-
// and provides a huge benefit. And... you should be able to trace down the specific
|
|
6
|
+
// NOTE: While mapping line numbers would be nice, just adding the file is easier,
|
|
7
|
+
// and still provides a huge benefit. And... you should be able to trace down the specific
|
|
7
8
|
// line from the message (and if not, you should make the message more specific).
|
|
8
9
|
compileTransformBefore((contents, path, module) => {
|
|
9
10
|
if (![".js", ".jsx", ".ts", ".tsx"].some(x => path.endsWith(x))) {
|
|
@@ -34,21 +35,10 @@ if (isNode()) {
|
|
|
34
35
|
return newConsole;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
-
// so this makes them accurate for original source.
|
|
39
|
-
contents = contents.split("\n").map((line, index) => {
|
|
40
|
-
return line
|
|
41
|
-
.replaceAll(`(0, diskLogger_1.diskLog)(`, `(0, diskLogger_1.diskLog)(console.__context, { __LINE__: ${index} }, `)
|
|
42
|
-
.replaceAll(` diskLog(`, ` diskLog(console.__context, { __LINE__: ${index} },`)
|
|
43
|
-
.replaceAll(`(0, diskLogger_1.logDisk)(`, `(0, diskLogger_1.logDisk)(console.__context, { __LINE__: ${index} }, `)
|
|
44
|
-
.replaceAll(` logDisk(`, ` logDisk(console.__context, { __LINE__: ${index} },`)
|
|
45
|
-
;
|
|
46
|
-
}).join("\n");
|
|
47
|
-
// IMPORTANT! No newlines, so the line numbers match up with the original source.
|
|
38
|
+
// IMPORTANT! No newlines, so we don't break sourcemaps
|
|
48
39
|
return `var console = (${shimConsole.toString().replaceAll("\n", " ")})(); ${contents}`;
|
|
49
40
|
});
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
42
|
+
let diskShimConsoleLogs = require("./diskShimConsoleLogs") as typeof import("./diskShimConsoleLogs");
|
|
43
|
+
diskShimConsoleLogs.shimConsoleLogs();
|
|
44
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Huge amount of data:
|
|
2
|
+
https://127-0-0-1.querysubtest.com:7007/?hot&enableLogs&page=login&caseInsensitive&showingmanagement&endTime=1757732880000&startTime=1746997620000&managementpage=LogViewer2
|
|
3
|
+
Very small amount of data
|
|
4
|
+
https://127-0-0-1.querysubtest.com:7007/?hot&enableLogs&page=login&filter=%22431%22&showingmanagement&endTime=1755140880000&startTime=1754950020000&managementpage=LogViewer2
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
4) Setup new hetnzer server
|
|
9
|
+
yarn setup-machine 176.9.2.136
|
|
10
|
+
5) Update all services, and move them to that machine
|
|
11
|
+
5) Verify the hezner server can run the site well
|
|
12
|
+
6) Take down our digital ocean server
|
|
13
|
+
7) Destroy our digital ocean server
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
6) Update URLParam to allow linking it to other parameters, resetting when they change.
|
|
17
|
+
- With a function, and have standard one beside URLParam (that uses page and tab)
|
|
18
|
+
- ALSO managementPageURL
|
|
19
|
+
- Reset filter in FastArchiveViewer
|
|
20
|
+
- First observe the overlap with it and the BookOverview
|
|
21
|
+
- If we are actually on the book overview page and we close the management page then that shouldn't reset it. We just want to reset it when you change pages. Because we want you to be able to hide and show the management page quickly if you want to double check something. Generally speaking though, you won't be on a page with filter and then going back and forth. And if you are, whatever. That's just the management page. We just want to avoid the overall confusion and annoyance of having lots of pre-filled values (And mostly the confusion of having filter prefilled all the time because it's always going to be set because everyone uses it and no one resets it at the moment.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
5) IM error notifications - allow immediately knowing about production issues, for better testing
|
|
27
|
+
- Create a dedicated entry point which acts like a client of the HTTP server, using RecentErrorControllers.getRecentErrors
|
|
28
|
+
- Getting it working in a script will be interesting, but... in theory it should just work?
|
|
29
|
+
- Just for new errors
|
|
30
|
+
- Using backblaze to track when we send it, so we can heavily limit IMs and email
|
|
31
|
+
- IM api key tracked in secrets (like email api key)
|
|
32
|
+
- Once we get it working, deploy to production
|
|
33
|
+
|
|
34
|
+
6) IM + email digests (daily / weekly?)
|
|
35
|
+
- a very short digest for the instant message which then links to a page on the site with a larger digest
|
|
36
|
+
- which has tabs, and each part in the instant message links to the correct tab
|
|
37
|
+
- Augments the error notifications entry point, having it also queue stuff up for digests.
|
|
38
|
+
- Some notifications will never be immediate and will always be only in digests.
|
|
39
|
+
- For now this will just be for:
|
|
40
|
+
- non-suppressed errors
|
|
41
|
+
- suppressed errors
|
|
42
|
+
- Eventually the goal of this is to add our tracking charts to this. There are some really useful metrics we can track.
|
|
43
|
+
- unique visit IPs.
|
|
44
|
+
- Percent bounces.
|
|
45
|
+
- average visit length and also median visit length top 95" bottom 5"
|
|
46
|
+
- average stories read average story percentages read
|
|
47
|
+
- percent of first page story views by subscribers percent of fifth page story view by subscribers
|
|
48
|
+
- Number of users visiting the fifth page.
|
|
49
|
+
- New subscriptions
|
|
50
|
+
- Subscription cancellations
|
|
51
|
+
- Total subscriptions
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
5) Life cycle analyzer
|
|
56
|
+
- Uses FastArchiveViewer, but instead of showing a table, shows lifecycles (a derived concept)
|
|
57
|
+
- We save them in backblaze, with a bit of cache for loading them
|
|
58
|
+
- List of life cycles
|
|
59
|
+
- Life cycle
|
|
60
|
+
- Title
|
|
61
|
+
- Operation list (each supports | / &, but having multiple is even better)
|
|
62
|
+
- Match filter
|
|
63
|
+
- Group key extractions (optional, if not set it becomes a singleton)
|
|
64
|
+
- Just a field name
|
|
65
|
+
- CAN have multiple, which adds us as multiple life cycles
|
|
66
|
+
- With each one being namespaced using the key, so we can tell them apart
|
|
67
|
+
- Global value setting (optional, if not set it has no global state impact)
|
|
68
|
+
- A list of set values
|
|
69
|
+
- Each one is an expression which can use fields in the object, ex:
|
|
70
|
+
- `alivePathValueServers.$threadId = true`
|
|
71
|
+
- Show AND SHOULDN'T include match filters!
|
|
72
|
+
- So when we should the count matched, we can show stats for these, which will often be should have "finished", and shouldn't have "error", so we can see completed, and errors
|
|
73
|
+
- Similar to error notifications, but... it's nice to also have this here, as we could miss the notification, or suppress it, but when we are looking at a life cycle it's relevant skyrockets.
|
|
74
|
+
- ALSO for start, so we can see cutoff starts!
|
|
75
|
+
OH! How do we handle cut off starts?
|
|
76
|
+
- Maybe... we have a "read preload" duration, and... we read that, BUT, only include life cycles which are also in our main selected time. So we don't cut anything off in our main time, but don't add new values which also get cut off!
|
|
77
|
+
- Same time/machine/thread selector as log viewer
|
|
78
|
+
- Allow filtering to specific life cycles
|
|
79
|
+
- After download, shows matches per life cycle
|
|
80
|
+
- Button to reset to all
|
|
81
|
+
- Download logs, and scan for selected life cycles
|
|
82
|
+
- Immediately on load, showing progress and throttling, so it's not too bad
|
|
83
|
+
- Result
|
|
84
|
+
- List of life cycles, with count of each
|
|
85
|
+
- Table of individual life cycles?
|
|
86
|
+
- Can then filter within these life cycles by searching
|
|
87
|
+
- BUT, importantly, if any log is matched in a life cycle, the entire life cycle is matched
|
|
88
|
+
- AND, global settings for ALL life cycles are applied, not just filtered ones!
|
|
89
|
+
- Table of result life cycles
|
|
90
|
+
- Preview shows first matched line
|
|
91
|
+
- ALSO, shows duration of life cycle!
|
|
92
|
+
- And start date
|
|
93
|
+
- Expand to see pin that specific life cycle above
|
|
94
|
+
- Show list of logs in it, in another table, with searching on each of them
|
|
95
|
+
- Start date AND duration of each line!
|
|
96
|
+
- Can pin multiple life cycles (I guess might as well)
|
|
97
|
+
- Show list of global value expressions as well (limited, but with filtering to search them easily)
|
|
98
|
+
- Can expand a global value to see object (but again... limited?)
|
|
99
|
+
- Can select a specific global value path, to have it injected into
|
|
100
|
+
- a column for before and after each life cycle
|
|
101
|
+
- A column for after each line within a life cycle
|
|
102
|
+
- Can also select parent values, to show all paths under that (use ShowMore, to make them manageable...)
|
|
103
|
+
|
|
104
|
+
6) Add life cycles for
|
|
105
|
+
- Node discovery life cycle (new node, check for alive, check for paths, etc)
|
|
106
|
+
- Discover nodes
|
|
107
|
+
- Check if trusted
|
|
108
|
+
- Sets authorities
|
|
109
|
+
- Sets nodes which exist
|
|
110
|
+
- Ready broadcast
|
|
111
|
+
- Resync is also part of life cycle
|
|
112
|
+
- Use different keys for threadId, and, triggeredNodeId, so we can track how a node discovers other nodes, vs how a node is discovered
|
|
113
|
+
- Mark as dead, dead count increases, remove node as dead
|
|
114
|
+
- MAYBE there should be 2 lifecycles, one for all, and one for just creation type stuff (no discovery)
|
|
115
|
+
- Trusted machine lifecycle
|
|
116
|
+
- Check if we need to add trust
|
|
117
|
+
- Add trust to archives
|
|
118
|
+
- Read trusted nodes from archives
|
|
119
|
+
=> Set of trusted machines per threadId
|
|
120
|
+
- Functions evaluation in FunctionRunner
|
|
121
|
+
- Adding in http, evaluation, and maybe even forwarding the final PathValue writes to it?
|
|
122
|
+
- Known nodes
|
|
123
|
+
- Not really a life cycle, but does set global state
|
|
124
|
+
- Value syncing
|
|
125
|
+
- Crosses node boundaries, with the server having to include the client key, so we can track a proxy access => ask the server => validate permissions => send value => receive value => notify proxy
|
|
126
|
+
- Value creation
|
|
127
|
+
- Source, propagation, writing to archives
|
|
128
|
+
- Permissions (probably part of value syncing)
|
|
129
|
+
- SOCKET connections, open, stats, closed, error, etc.
|
|
130
|
+
- Archive management
|
|
131
|
+
- Queue (count)
|
|
132
|
+
- Archive (count + file)
|
|
133
|
+
- Joining, deleting, etc, etc
|
|
134
|
+
|
|
135
|
+
6) Redeploy everything to the servers again, updating our machine apply script, all of our services, deploying our code, etc
|
|
136
|
+
|
|
137
|
+
7) Use log viewer to find rejections / errors during startup, and then trace down the sync life cycle for the path write, and find where it is lagging out!
|
|
138
|
+
- Re-enable all of our services as well.
|
|
139
|
+
- Just logs MIGHT be able to do it, but... life cycles should make it a lot easier to correlate logs, which is something we need to do anyways to solve it...
|
|
140
|
+
|
|
141
|
+
10) Verify old user/fast-log-cache machine folders are deleted
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
SPECIAL UI links for certain errors in log view
|
|
145
|
+
- Probably dynamically created, based on contents of log
|
|
146
|
+
- LINKS to filters for all these special errors on a special page
|
|
147
|
+
- Special errors
|
|
148
|
+
- Rejection errors
|
|
149
|
+
- Link to life cycle filtering show the life cycles involving the path (we need to hardcode the life cycle ids for these, after we create the life cycles)
|
|
150
|
+
- Audit failures
|
|
151
|
+
- Same info as rejection errors, except for the path, and the failing lock path
|