@liquidmetal-ai/raindrop 0.5.1 → 0.5.2
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/README.md +117 -56
- package/dist/codegen.js +3 -3
- package/dist/commands/build/find.js +2 -2
- package/dist/commands/log/query.d.ts +27 -0
- package/dist/commands/log/query.d.ts.map +1 -0
- package/dist/commands/log/query.js +231 -0
- package/dist/commands/log/tail.d.ts +22 -0
- package/dist/commands/log/tail.d.ts.map +1 -0
- package/dist/commands/log/tail.js +214 -0
- package/oclif.manifest.json +2351 -1913
- package/package.json +3 -3
- package/templates/db/node_modules/.bin/prisma +2 -2
- package/templates/db/node_modules/.bin/prisma-kysely +2 -2
- package/templates/db/node_modules/.bin/tsc +2 -2
- package/templates/db/node_modules/.bin/tsserver +2 -2
- package/templates/db/node_modules/.bin/zx +2 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { valueOf } from '@liquidmetal-ai/drizzle/appify/build';
|
|
2
|
+
import { create } from '@bufbuild/protobuf';
|
|
3
|
+
import { QueryLogsRequestSchema, TimeQuerySchema } from '@liquidmetal-ai/drizzle/liquidmetal/v1alpha1/riverjack_pb';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import { BaseCommand } from '../../base-command.js';
|
|
6
|
+
export default class Query extends BaseCommand {
|
|
7
|
+
static args = {};
|
|
8
|
+
static description = 'query logs of applications with time range filtering';
|
|
9
|
+
static examples = [
|
|
10
|
+
`<%= config.bin %> <%= command.id %>`,
|
|
11
|
+
`<%= config.bin %> <%= command.id %> --application my-app`,
|
|
12
|
+
`<%= config.bin %> <%= command.id %> --application my-app --version v1.2.3`,
|
|
13
|
+
`<%= config.bin %> <%= command.id %> --application my-app --start-time 1638360000000 --end-time 1638363600000`,
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.HIDDEN_FLAGS,
|
|
17
|
+
output: Flags.string({
|
|
18
|
+
char: 'o',
|
|
19
|
+
description: 'output format',
|
|
20
|
+
default: 'text',
|
|
21
|
+
options: ['text', 'json'],
|
|
22
|
+
}),
|
|
23
|
+
impersonate: Flags.string({
|
|
24
|
+
char: 'i',
|
|
25
|
+
description: 'impersonate organization',
|
|
26
|
+
required: false,
|
|
27
|
+
hidden: true,
|
|
28
|
+
}),
|
|
29
|
+
manifest: Flags.string({
|
|
30
|
+
char: 'm',
|
|
31
|
+
description: 'project manifest',
|
|
32
|
+
required: false,
|
|
33
|
+
default: 'raindrop.manifest',
|
|
34
|
+
hidden: true,
|
|
35
|
+
}),
|
|
36
|
+
application: Flags.string({
|
|
37
|
+
char: 'a',
|
|
38
|
+
description: 'application',
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
version: Flags.string({
|
|
42
|
+
char: 'v',
|
|
43
|
+
description: 'application version',
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
startTime: Flags.string({
|
|
47
|
+
char: 's',
|
|
48
|
+
description: 'start time for query (Unix timestamp in milliseconds or ISO string)',
|
|
49
|
+
required: false,
|
|
50
|
+
}),
|
|
51
|
+
endTime: Flags.string({
|
|
52
|
+
char: 'e',
|
|
53
|
+
description: 'end time for query (Unix timestamp in milliseconds or ISO string)',
|
|
54
|
+
required: false,
|
|
55
|
+
}),
|
|
56
|
+
last: Flags.string({
|
|
57
|
+
char: 'l',
|
|
58
|
+
description: 'query logs from last duration (e.g., "1h", "30m", "2d")',
|
|
59
|
+
required: false,
|
|
60
|
+
}),
|
|
61
|
+
rainbowAuthService: Flags.string({
|
|
62
|
+
default: 'https://liquidmetal.run/api/connect',
|
|
63
|
+
hidden: true,
|
|
64
|
+
env: 'LIQUIDMETAL_RAINBOW_AUTH_SERVICE',
|
|
65
|
+
}),
|
|
66
|
+
raindropCatalogService: Flags.string({
|
|
67
|
+
env: 'RAINDROP_CATALOG_SERVICE',
|
|
68
|
+
description: 'URL of the catalog service',
|
|
69
|
+
hidden: true,
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
parseTimeToMillis(timeStr) {
|
|
73
|
+
// Try parsing as Unix timestamp first
|
|
74
|
+
const timestamp = Number(timeStr);
|
|
75
|
+
if (!isNaN(timestamp)) {
|
|
76
|
+
// If it's a reasonable number, assume it's already in milliseconds
|
|
77
|
+
if (timestamp > 1000000000000) {
|
|
78
|
+
return BigInt(timestamp);
|
|
79
|
+
}
|
|
80
|
+
// If it's smaller, assume it's in seconds and convert to milliseconds
|
|
81
|
+
return BigInt(timestamp * 1000);
|
|
82
|
+
}
|
|
83
|
+
// Try parsing as ISO string
|
|
84
|
+
const date = new Date(timeStr);
|
|
85
|
+
if (!isNaN(date.getTime())) {
|
|
86
|
+
return BigInt(date.getTime());
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Invalid time format: ${timeStr}. Use Unix timestamp (ms) or ISO string.`);
|
|
89
|
+
}
|
|
90
|
+
parseDurationToMillis(duration) {
|
|
91
|
+
const match = duration.match(/^(\d+)([smhd])$/);
|
|
92
|
+
console.log('Match Given');
|
|
93
|
+
console.log(match);
|
|
94
|
+
if (!match) {
|
|
95
|
+
throw new Error(`Invalid duration format: ${duration}. Use format like "1h", "30m", "2d".`);
|
|
96
|
+
}
|
|
97
|
+
const value = parseInt(match[1], 10);
|
|
98
|
+
const unit = match[2];
|
|
99
|
+
switch (unit) {
|
|
100
|
+
case 's':
|
|
101
|
+
return value * 1000;
|
|
102
|
+
case 'm':
|
|
103
|
+
return value * 60 * 1000;
|
|
104
|
+
case 'h':
|
|
105
|
+
return value * 60 * 60 * 1000;
|
|
106
|
+
case 'd':
|
|
107
|
+
return value * 24 * 60 * 60 * 1000;
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`Unsupported duration unit: ${unit}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async run() {
|
|
113
|
+
// Load version from config if not provided
|
|
114
|
+
const apps = await this.loadManifest();
|
|
115
|
+
const config = await this.loadConfig();
|
|
116
|
+
if (!this.flags.version) {
|
|
117
|
+
if (!config.versionId) {
|
|
118
|
+
this.error('No version provided or found in config', { exit: 1 });
|
|
119
|
+
}
|
|
120
|
+
this.flags.version = config.versionId;
|
|
121
|
+
}
|
|
122
|
+
// Load application from manifest if not provided
|
|
123
|
+
if (!this.flags.application) {
|
|
124
|
+
const app = apps[0];
|
|
125
|
+
if (app === undefined) {
|
|
126
|
+
this.error('No application provided or found in manifest', { exit: 1 });
|
|
127
|
+
}
|
|
128
|
+
this.flags.application = valueOf(app.name);
|
|
129
|
+
}
|
|
130
|
+
const { userId, client: riverjackService, organizationId: defaultOrganizationId, } = await this.tenantRiverjackService();
|
|
131
|
+
const organizationId = this.flags.impersonate ?? defaultOrganizationId;
|
|
132
|
+
// Calculate time range
|
|
133
|
+
let startTime;
|
|
134
|
+
let endTime;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (this.flags.last) {
|
|
137
|
+
// Use "last" duration
|
|
138
|
+
try {
|
|
139
|
+
const durationMs = this.parseDurationToMillis(this.flags.last);
|
|
140
|
+
startTime = BigInt(now - durationMs);
|
|
141
|
+
endTime = BigInt(now);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.error(`Invalid duration: ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (this.flags.startTime || this.flags.endTime) {
|
|
149
|
+
// Use explicit start/end times
|
|
150
|
+
try {
|
|
151
|
+
startTime = this.flags.startTime ? this.parseTimeToMillis(this.flags.startTime) : BigInt(now - 60 * 60 * 1000); // Default to 1 hour ago
|
|
152
|
+
endTime = this.flags.endTime ? this.parseTimeToMillis(this.flags.endTime) : BigInt(now);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.error(`Invalid time format: ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Default to last hour
|
|
161
|
+
startTime = BigInt(now - 60 * 60 * 1000);
|
|
162
|
+
endTime = BigInt(now);
|
|
163
|
+
}
|
|
164
|
+
this.log(`Using organization: ${organizationId}`);
|
|
165
|
+
this.log(`Using user: ${userId}`);
|
|
166
|
+
this.log(`Querying logs for ${this.flags.application}@${this.flags.version}`);
|
|
167
|
+
this.log(`Time range: ${new Date(Number(startTime)).toLocaleString()} - ${new Date(Number(endTime)).toLocaleString()}`);
|
|
168
|
+
this.log('');
|
|
169
|
+
try {
|
|
170
|
+
const timeQuery = create(TimeQuerySchema, {
|
|
171
|
+
startTime,
|
|
172
|
+
endTime,
|
|
173
|
+
});
|
|
174
|
+
const request = create(QueryLogsRequestSchema, {
|
|
175
|
+
organizationId,
|
|
176
|
+
userId,
|
|
177
|
+
applicationName: this.flags.application,
|
|
178
|
+
applicationVersionId: this.flags.version,
|
|
179
|
+
timeQuery,
|
|
180
|
+
});
|
|
181
|
+
const response = await riverjackService.queryLogs(request);
|
|
182
|
+
if (this.flags.output === 'json') {
|
|
183
|
+
this.log(JSON.stringify(response, null, 2));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (response.messages.length === 0) {
|
|
187
|
+
this.log('No log messages found for the specified criteria.');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.log(`Found ${response.messages.length} log messages:\n`);
|
|
191
|
+
response.messages.forEach((message, index) => {
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(message);
|
|
194
|
+
// Format structured log messages
|
|
195
|
+
if (parsed.timestamp) {
|
|
196
|
+
const timestamp = new Date(parsed.timestamp).toLocaleTimeString();
|
|
197
|
+
const level = parsed.level || 'INFO';
|
|
198
|
+
const msg = parsed.message || parsed.msg || message;
|
|
199
|
+
this.log(`[${timestamp}] ${level}: ${msg}`);
|
|
200
|
+
// Show additional fields if present
|
|
201
|
+
Object.entries(parsed).forEach(([key, value]) => {
|
|
202
|
+
if (!['timestamp', 'level', 'message', 'msg'].includes(key) && value !== undefined) {
|
|
203
|
+
this.log(` ${key}: ${JSON.stringify(value)}`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// If not structured, just display the message with index
|
|
209
|
+
this.log(`Message ${index + 1}: ${message}`);
|
|
210
|
+
}
|
|
211
|
+
this.log('');
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// If message isn't JSON, display it as-is with index
|
|
215
|
+
this.log(`Message ${index + 1}: ${message}`);
|
|
216
|
+
this.log('');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof Error) {
|
|
224
|
+
this.error(`Failed to query logs: ${error.message}`, { exit: 1 });
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.error('Failed to query logs: Unknown error', { exit: 1 });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class Tail extends BaseCommand<typeof Tail> {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
impersonate: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
manifest: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
application: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
version: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
rainbowAuthService: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
raindropCatalogService: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
config: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
rainbowAuthToken: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
rainbowOrganizationId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
rainbowUserId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
sendVersionMetadata: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
};
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=tail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tail.d.ts","sourceRoot":"","sources":["../../../src/commands/log/tail.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,WAAW,CAAC,OAAO,IAAI,CAAC;IACxD,MAAM,CAAC,IAAI,KAAM;IAEjB,MAAM,CAAC,WAAW,SAAwC;IAE1D,MAAM,CAAC,QAAQ,WAIb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;;MAyCV;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA0J3B"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { valueOf } from '@liquidmetal-ai/drizzle/appify/build';
|
|
2
|
+
import { create } from '@bufbuild/protobuf';
|
|
3
|
+
import { StreamLogsRequestSchema } from '@liquidmetal-ai/drizzle/liquidmetal/v1alpha1/riverjack_pb';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import { BaseCommand } from '../../base-command.js';
|
|
6
|
+
export default class Tail extends BaseCommand {
|
|
7
|
+
static args = {};
|
|
8
|
+
static description = 'tail logs of applications deployed';
|
|
9
|
+
static examples = [
|
|
10
|
+
`<%= config.bin %> <%= command.id %>`,
|
|
11
|
+
`<%= config.bin %> <%= command.id %> --application my-app`,
|
|
12
|
+
`<%= config.bin %> <%= command.id %> --application my-app --version v1.2.3`,
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...BaseCommand.HIDDEN_FLAGS,
|
|
16
|
+
output: Flags.string({
|
|
17
|
+
char: 'o',
|
|
18
|
+
description: 'output format',
|
|
19
|
+
default: 'text',
|
|
20
|
+
options: ['text', 'json'],
|
|
21
|
+
}),
|
|
22
|
+
impersonate: Flags.string({
|
|
23
|
+
char: 'i',
|
|
24
|
+
description: 'impersonate organization',
|
|
25
|
+
required: false,
|
|
26
|
+
hidden: true,
|
|
27
|
+
}),
|
|
28
|
+
manifest: Flags.string({
|
|
29
|
+
char: 'm',
|
|
30
|
+
description: 'project manifest',
|
|
31
|
+
required: false,
|
|
32
|
+
default: 'raindrop.manifest',
|
|
33
|
+
hidden: true,
|
|
34
|
+
}),
|
|
35
|
+
application: Flags.string({
|
|
36
|
+
char: 'a',
|
|
37
|
+
description: 'application',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
version: Flags.string({
|
|
41
|
+
char: 'v',
|
|
42
|
+
description: 'application version',
|
|
43
|
+
required: false,
|
|
44
|
+
}),
|
|
45
|
+
rainbowAuthService: Flags.string({
|
|
46
|
+
default: 'https://liquidmetal.run/api/connect',
|
|
47
|
+
hidden: true,
|
|
48
|
+
env: 'LIQUIDMETAL_RAINBOW_AUTH_SERVICE',
|
|
49
|
+
}),
|
|
50
|
+
raindropCatalogService: Flags.string({
|
|
51
|
+
env: 'RAINDROP_CATALOG_SERVICE',
|
|
52
|
+
description: 'URL of the catalog service',
|
|
53
|
+
hidden: true,
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
async run() {
|
|
57
|
+
// Load version from config if not provided
|
|
58
|
+
const apps = await this.loadManifest();
|
|
59
|
+
const config = await this.loadConfig();
|
|
60
|
+
if (!this.flags.version) {
|
|
61
|
+
if (!config.versionId) {
|
|
62
|
+
this.error('No version provided or found in config', { exit: 1 });
|
|
63
|
+
}
|
|
64
|
+
this.flags.version = config.versionId;
|
|
65
|
+
}
|
|
66
|
+
// Load application from manifest if not provided
|
|
67
|
+
if (!this.flags.application) {
|
|
68
|
+
const app = apps[0];
|
|
69
|
+
if (app === undefined) {
|
|
70
|
+
this.error('No application provided or found in manifest', { exit: 1 });
|
|
71
|
+
}
|
|
72
|
+
this.flags.application = valueOf(app.name);
|
|
73
|
+
}
|
|
74
|
+
const { userId, client: riverjackService, organizationId: defaultOrganizationId, } = await this.tenantRiverjackService();
|
|
75
|
+
const organizationId = this.flags.impersonate ?? defaultOrganizationId;
|
|
76
|
+
this.log(`Using organization: ${organizationId}`);
|
|
77
|
+
this.log(`Using user: ${userId}`);
|
|
78
|
+
this.log(`Tailing logs for ${this.flags.application}@${this.flags.version} using Riverjack service...`);
|
|
79
|
+
this.log('Press Ctrl+C to stop\n');
|
|
80
|
+
try {
|
|
81
|
+
const request = create(StreamLogsRequestSchema, {
|
|
82
|
+
organizationId,
|
|
83
|
+
userId,
|
|
84
|
+
applicationName: this.flags.application,
|
|
85
|
+
applicationVersionId: this.flags.version,
|
|
86
|
+
});
|
|
87
|
+
for await (const resp of riverjackService.streamLogs(request)) {
|
|
88
|
+
if (resp.message) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(resp.message);
|
|
91
|
+
// Filter out heartbeat messages unless in JSON mode
|
|
92
|
+
if (parsed.type === 'heartbeat' && this.flags.output !== 'json') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (this.flags.output === 'json') {
|
|
96
|
+
this.log(JSON.stringify(resp, null, 2));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Format different message types appropriately
|
|
100
|
+
switch (parsed.type) {
|
|
101
|
+
case 'stream_started':
|
|
102
|
+
this.log(`🔄 Started streaming logs for ${parsed.applicationName}@${parsed.applicationVersionId}`);
|
|
103
|
+
this.log(` Organization: ${parsed.organizationId}`);
|
|
104
|
+
this.log(` User: ${parsed.userId}`);
|
|
105
|
+
this.log(` Time: ${new Date(parsed.timestamp).toLocaleString()}`);
|
|
106
|
+
break;
|
|
107
|
+
case 'new_events':
|
|
108
|
+
// Just show the events without the counter header
|
|
109
|
+
if (parsed.metrics) {
|
|
110
|
+
this.log(` Total events in system: ${parsed.metrics.totalEvents}`);
|
|
111
|
+
this.log(` Last processed: ${parsed.metrics.lastProcessed ? new Date(parsed.metrics.lastProcessed).toLocaleString() : 'N/A'}`);
|
|
112
|
+
}
|
|
113
|
+
if (parsed.events && parsed.events.length > 0) {
|
|
114
|
+
parsed.events.forEach((event) => {
|
|
115
|
+
const timestamp = event.storedAt ? new Date(event.storedAt).toLocaleTimeString() : 'Unknown time';
|
|
116
|
+
const eventName = event.name || 'Event';
|
|
117
|
+
const traceId = event.trace && typeof event.trace === 'object' ? event.trace.event_id : 'N/A';
|
|
118
|
+
const status = event.status || 'unknown';
|
|
119
|
+
const duration = event.trace && typeof event.trace === 'object' ?
|
|
120
|
+
event.trace.end_ms - event.trace.start_ms : null;
|
|
121
|
+
this.log(` [${timestamp}] ${eventName} (${status}) - Trace: ${traceId}${duration ? ` - Duration: ${duration}ms` : ''}`);
|
|
122
|
+
// Show attributes with better formatting
|
|
123
|
+
if (event.attributes && typeof event.attributes === 'object') {
|
|
124
|
+
const attrs = event.attributes;
|
|
125
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
126
|
+
if (value !== undefined && value !== null) {
|
|
127
|
+
// Format common attributes nicely
|
|
128
|
+
if (key === 'http.status') {
|
|
129
|
+
const statusCode = value;
|
|
130
|
+
const statusEmoji = statusCode >= 200 && statusCode < 300 ? '✅' : statusCode >= 400 ? '❌' : '⚠️';
|
|
131
|
+
this.log(` ${key}: ${statusEmoji} ${value}`);
|
|
132
|
+
}
|
|
133
|
+
else if (key === 'http.method') {
|
|
134
|
+
this.log(` ${key}: ${value}`);
|
|
135
|
+
}
|
|
136
|
+
else if (key === 'http.url') {
|
|
137
|
+
this.log(` ${key}: ${value}`);
|
|
138
|
+
}
|
|
139
|
+
else if (key === 'query') {
|
|
140
|
+
// SQL query formatting
|
|
141
|
+
this.log(` 🗄️ SQL Query: ${value}`);
|
|
142
|
+
}
|
|
143
|
+
else if (key === 'rows_read') {
|
|
144
|
+
this.log(` 📊 Rows Read: ${value}`);
|
|
145
|
+
}
|
|
146
|
+
else if (key === 'rows_written') {
|
|
147
|
+
this.log(` ✍️ Rows Written: ${value}`);
|
|
148
|
+
}
|
|
149
|
+
else if (key === 'db') {
|
|
150
|
+
this.log(` 🏦 Database: ${value}`);
|
|
151
|
+
}
|
|
152
|
+
else if (key === 'error' && typeof eventName === 'string' && eventName.includes('sql')) {
|
|
153
|
+
this.log(` ❌ SQL Error: ${value}`);
|
|
154
|
+
}
|
|
155
|
+
else if (key === 'meta' && typeof value === 'object') {
|
|
156
|
+
// SQL metadata
|
|
157
|
+
const meta = value;
|
|
158
|
+
if (meta.rows_read)
|
|
159
|
+
this.log(` 📊 Rows Read: ${meta.rows_read}`);
|
|
160
|
+
if (meta.rows_written)
|
|
161
|
+
this.log(` ✍️ Rows Written: ${meta.rows_written}`);
|
|
162
|
+
if (meta.duration)
|
|
163
|
+
this.log(` ⏱️ Query Duration: ${meta.duration}ms`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
this.log(` ${key}: ${JSON.stringify(value)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Show organization/application context
|
|
172
|
+
if (event.organization || event.application) {
|
|
173
|
+
const org = event.organization && typeof event.organization === 'object' ? event.organization.id : 'N/A';
|
|
174
|
+
const app = event.application && typeof event.application === 'object' ? event.application.name : 'N/A';
|
|
175
|
+
const version = event.application && typeof event.application === 'object' && typeof event.application.version === 'object' ?
|
|
176
|
+
event.application.version.id : 'N/A';
|
|
177
|
+
this.log(` Context: org=${org}, app=${app}, version=${version}`);
|
|
178
|
+
}
|
|
179
|
+
this.log(''); // Empty line for readability
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
case 'stream_ended':
|
|
184
|
+
this.log(`✅ Stream ended after ${Math.round(parsed.duration / 1000)}s (${parsed.totalPolls} polls)`);
|
|
185
|
+
this.log(` Subscriber ID: ${parsed.subscriberId}`);
|
|
186
|
+
this.log(` End time: ${new Date(parsed.timestamp).toLocaleString()}`);
|
|
187
|
+
break;
|
|
188
|
+
case 'service_error':
|
|
189
|
+
this.log(`❌ Service error: ${parsed.error}`);
|
|
190
|
+
this.log(` Time: ${new Date(parsed.timestamp).toLocaleString()}`);
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
// For any other message types, just display the raw message
|
|
194
|
+
this.log(resp.message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// If message isn't JSON, just display it as-is
|
|
200
|
+
this.log(resp.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof Error) {
|
|
207
|
+
this.error(`Failed to tail logs: ${error.message}`, { exit: 1 });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
this.error('Failed to tail logs: Unknown error', { exit: 1 });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|