@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.
@@ -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
+ }