@gokiteam/goki-dev 0.2.0 → 0.2.1
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/Dockerfile.backend +38 -0
- package/Dockerfile.dev +38 -0
- package/Dockerfile.frontend +37 -0
- package/client/dist/client.d.ts +48 -0
- package/client/dist/client.js +51 -1
- package/client/dist/types.d.ts +59 -0
- package/docker-compose.dev.yml +81 -0
- package/docker-compose.services.yml +186 -0
- package/docker-compose.yml +233 -0
- package/nginx.conf +41 -0
- package/package.json +13 -2
- package/src/api/httpTraffic/Controllers.js +7 -0
- package/src/api/httpTraffic/Logic.js +28 -0
- package/src/api/httpTraffic/Router.js +1 -0
- package/src/api/httpTraffic/Schemas.js +15 -1
- package/src/mcp/tools/httpTraffic.js +20 -1
- package/src/singletons/HttpProxy.js +11 -1
- package/src/singletons/SqliteStore.js +6 -2
- package/ui/Dockerfile.dev +19 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install Java JRE 21 for Firebase Emulator + build tools for better-sqlite3 + Docker CLI + PostgreSQL client
|
|
6
|
+
RUN apk add --no-cache openjdk21-jre bash python3 make g++ docker-cli postgresql-client
|
|
7
|
+
|
|
8
|
+
# Install Firebase CLI globally for Firestore emulator
|
|
9
|
+
RUN npm install -g firebase-tools
|
|
10
|
+
|
|
11
|
+
# Copy package files and npm config
|
|
12
|
+
COPY package*.json ./
|
|
13
|
+
|
|
14
|
+
# Build arg for npm authentication
|
|
15
|
+
ARG NPM_TOKEN
|
|
16
|
+
ENV NPM_TOKEN=${NPM_TOKEN}
|
|
17
|
+
COPY .npmrc ./
|
|
18
|
+
|
|
19
|
+
# Install dependencies (including optional native deps like better-sqlite3)
|
|
20
|
+
RUN npm install --omit=dev --include=optional
|
|
21
|
+
|
|
22
|
+
# Copy source code
|
|
23
|
+
COPY src/ ./src/
|
|
24
|
+
COPY config.development ./
|
|
25
|
+
COPY config.test ./
|
|
26
|
+
|
|
27
|
+
# Copy Firebase configuration
|
|
28
|
+
COPY firebase.json ./
|
|
29
|
+
COPY firestore.rules ./
|
|
30
|
+
|
|
31
|
+
# Create data and logs directories
|
|
32
|
+
RUN mkdir -p /app/data /app/logs /app/data/firestore
|
|
33
|
+
|
|
34
|
+
# Expose ports
|
|
35
|
+
EXPOSE 9000 8085 8087 8883 8080
|
|
36
|
+
|
|
37
|
+
# Start server directly (env vars set via docker-compose)
|
|
38
|
+
CMD ["node", "src/Server.js"]
|
package/Dockerfile.dev
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install Java JRE 21 for Firebase Emulator + build tools for better-sqlite3 + PostgreSQL client
|
|
6
|
+
RUN apk add --no-cache openjdk21-jre bash python3 make g++ postgresql-client
|
|
7
|
+
|
|
8
|
+
# Install Firebase CLI globally for Firestore emulator
|
|
9
|
+
RUN npm install -g firebase-tools
|
|
10
|
+
|
|
11
|
+
# Copy package files and npm config
|
|
12
|
+
COPY package*.json ./
|
|
13
|
+
|
|
14
|
+
# Build arg for npm authentication
|
|
15
|
+
ARG NPM_TOKEN
|
|
16
|
+
ENV NPM_TOKEN=${NPM_TOKEN}
|
|
17
|
+
COPY .npmrc ./
|
|
18
|
+
|
|
19
|
+
# Install ALL dependencies (including dev for watch mode)
|
|
20
|
+
RUN npm install
|
|
21
|
+
|
|
22
|
+
# Install nodemon for reliable file watching in Docker (fs.watch doesn't work with bind mounts)
|
|
23
|
+
RUN npm install -g nodemon
|
|
24
|
+
|
|
25
|
+
# Copy config files
|
|
26
|
+
COPY config.development ./
|
|
27
|
+
COPY config.test ./
|
|
28
|
+
COPY firebase.json ./
|
|
29
|
+
COPY firestore.rules ./
|
|
30
|
+
|
|
31
|
+
# Create data and logs directories
|
|
32
|
+
RUN mkdir -p /app/data /app/logs /app/data/firestore
|
|
33
|
+
|
|
34
|
+
# Expose ports
|
|
35
|
+
EXPOSE 9000 8085 8087 8883 8080
|
|
36
|
+
|
|
37
|
+
# Start server with watch mode using nodemon (polling for Docker bind mount compatibility)
|
|
38
|
+
CMD ["nodemon", "--watch", "src", "--ext", "js,json", "--legacy-watch", "--polling-interval", "1000", "src/Server.js"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Build stage
|
|
2
|
+
FROM node:20-alpine AS build
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Copy UI package files
|
|
7
|
+
COPY ui/package*.json ./ui/
|
|
8
|
+
|
|
9
|
+
# Install UI dependencies
|
|
10
|
+
WORKDIR /app/ui
|
|
11
|
+
RUN npm ci
|
|
12
|
+
|
|
13
|
+
# Copy UI source files
|
|
14
|
+
COPY ui/ ./
|
|
15
|
+
|
|
16
|
+
# Build the React app
|
|
17
|
+
# API URL will be injected at runtime via environment variable
|
|
18
|
+
RUN npm run build
|
|
19
|
+
|
|
20
|
+
# Production stage
|
|
21
|
+
FROM nginx:1.25-alpine
|
|
22
|
+
|
|
23
|
+
# Copy nginx configuration
|
|
24
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
25
|
+
|
|
26
|
+
# Copy built files from build stage
|
|
27
|
+
COPY --from=build /app/ui/build /usr/share/nginx/html
|
|
28
|
+
|
|
29
|
+
# Add healthcheck
|
|
30
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
31
|
+
CMD wget --quiet --tries=1 --spider http://127.0.0.1/health || exit 1
|
|
32
|
+
|
|
33
|
+
# Expose port
|
|
34
|
+
EXPOSE 80
|
|
35
|
+
|
|
36
|
+
# Start nginx
|
|
37
|
+
CMD ["nginx", "-g", "daemon off;"]
|
package/client/dist/client.d.ts
CHANGED
|
@@ -314,6 +314,53 @@ export declare class SchedulerClient {
|
|
|
314
314
|
traceId?: string;
|
|
315
315
|
}): Promise<Types.SchedulerTickResponse>;
|
|
316
316
|
}
|
|
317
|
+
export declare class HttpTrafficClient {
|
|
318
|
+
private client;
|
|
319
|
+
constructor(client: AxiosInstance);
|
|
320
|
+
private call;
|
|
321
|
+
list(params?: {
|
|
322
|
+
filter?: Types.HttpTrafficFilter;
|
|
323
|
+
limit?: number;
|
|
324
|
+
offset?: number;
|
|
325
|
+
traceId?: string;
|
|
326
|
+
}): Promise<{
|
|
327
|
+
entries: Types.HttpTrafficEntry[];
|
|
328
|
+
total: number;
|
|
329
|
+
limit: number;
|
|
330
|
+
offset: number;
|
|
331
|
+
} & {
|
|
332
|
+
traceId: string;
|
|
333
|
+
}>;
|
|
334
|
+
getDetails(params: {
|
|
335
|
+
id: string;
|
|
336
|
+
traceId?: string;
|
|
337
|
+
}): Promise<{
|
|
338
|
+
entry: Types.HttpTrafficEntry;
|
|
339
|
+
} & {
|
|
340
|
+
traceId: string;
|
|
341
|
+
}>;
|
|
342
|
+
clear(params?: {
|
|
343
|
+
traceId?: string;
|
|
344
|
+
}): Promise<{
|
|
345
|
+
message: string;
|
|
346
|
+
} & {
|
|
347
|
+
traceId: string;
|
|
348
|
+
}>;
|
|
349
|
+
waitFor(params: {
|
|
350
|
+
filter: Types.HttpTrafficWaitFilter;
|
|
351
|
+
timeout?: number;
|
|
352
|
+
traceId?: string;
|
|
353
|
+
}): Promise<{
|
|
354
|
+
entry: Types.HttpTrafficEntry;
|
|
355
|
+
foundAt: number;
|
|
356
|
+
traceId: string;
|
|
357
|
+
}>;
|
|
358
|
+
stats(params?: {
|
|
359
|
+
traceId?: string;
|
|
360
|
+
}): Promise<Types.HttpTrafficStats & {
|
|
361
|
+
traceId: string;
|
|
362
|
+
}>;
|
|
363
|
+
}
|
|
317
364
|
export declare class DevToolsClient {
|
|
318
365
|
private client;
|
|
319
366
|
private baseUrl;
|
|
@@ -326,6 +373,7 @@ export declare class DevToolsClient {
|
|
|
326
373
|
docker: DockerClient;
|
|
327
374
|
platform: PlatformClient;
|
|
328
375
|
scheduler: SchedulerClient;
|
|
376
|
+
httpTraffic: HttpTrafficClient;
|
|
329
377
|
constructor(config?: DevToolsClientConfig);
|
|
330
378
|
generateTraceId(prefix?: string): string;
|
|
331
379
|
}
|
package/client/dist/client.js
CHANGED
|
@@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
22
22
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.DevToolsClient = exports.SchedulerClient = exports.PlatformClient = exports.DockerClient = exports.MqttClient = exports.FirestoreClient = exports.RedisClient = exports.PostgresClient = exports.LoggingClient = exports.PubSubClient = void 0;
|
|
25
|
+
exports.DevToolsClient = exports.HttpTrafficClient = exports.SchedulerClient = exports.PlatformClient = exports.DockerClient = exports.MqttClient = exports.FirestoreClient = exports.RedisClient = exports.PostgresClient = exports.LoggingClient = exports.PubSubClient = void 0;
|
|
26
26
|
const axios_1 = __importDefault(require("axios"));
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// PubSub Client
|
|
@@ -473,6 +473,55 @@ class SchedulerClient {
|
|
|
473
473
|
}
|
|
474
474
|
exports.SchedulerClient = SchedulerClient;
|
|
475
475
|
// ============================================================================
|
|
476
|
+
// HTTP Traffic Client
|
|
477
|
+
// ============================================================================
|
|
478
|
+
class HttpTrafficClient {
|
|
479
|
+
constructor(client) {
|
|
480
|
+
this.client = client;
|
|
481
|
+
}
|
|
482
|
+
async call(endpoint, data = {}) {
|
|
483
|
+
const response = await this.client.post(endpoint, data);
|
|
484
|
+
return response.data;
|
|
485
|
+
}
|
|
486
|
+
async list(params = {}) {
|
|
487
|
+
const result = await this.call('/v1/http-traffic/list', {
|
|
488
|
+
filter: params.filter,
|
|
489
|
+
limit: params.limit,
|
|
490
|
+
offset: params.offset,
|
|
491
|
+
traceId: params.traceId
|
|
492
|
+
});
|
|
493
|
+
return result.data;
|
|
494
|
+
}
|
|
495
|
+
async getDetails(params) {
|
|
496
|
+
const result = await this.call('/v1/http-traffic/details', {
|
|
497
|
+
id: params.id,
|
|
498
|
+
traceId: params.traceId
|
|
499
|
+
});
|
|
500
|
+
return result.data;
|
|
501
|
+
}
|
|
502
|
+
async clear(params = {}) {
|
|
503
|
+
const result = await this.call('/v1/http-traffic/clear', {
|
|
504
|
+
traceId: params.traceId
|
|
505
|
+
});
|
|
506
|
+
return result.data;
|
|
507
|
+
}
|
|
508
|
+
async waitFor(params) {
|
|
509
|
+
const result = await this.call('/v1/http-traffic/wait-for', {
|
|
510
|
+
filter: params.filter,
|
|
511
|
+
timeout: params.timeout,
|
|
512
|
+
traceId: params.traceId
|
|
513
|
+
});
|
|
514
|
+
return result.data;
|
|
515
|
+
}
|
|
516
|
+
async stats(params = {}) {
|
|
517
|
+
const result = await this.call('/v1/http-traffic/stats', {
|
|
518
|
+
traceId: params.traceId
|
|
519
|
+
});
|
|
520
|
+
return result.data;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
exports.HttpTrafficClient = HttpTrafficClient;
|
|
524
|
+
// ============================================================================
|
|
476
525
|
// Main DevTools Client
|
|
477
526
|
// ============================================================================
|
|
478
527
|
class DevToolsClient {
|
|
@@ -494,6 +543,7 @@ class DevToolsClient {
|
|
|
494
543
|
this.docker = new DockerClient(this.client);
|
|
495
544
|
this.platform = new PlatformClient(this.client);
|
|
496
545
|
this.scheduler = new SchedulerClient(this.client);
|
|
546
|
+
this.httpTraffic = new HttpTrafficClient(this.client);
|
|
497
547
|
}
|
|
498
548
|
// ============================================================================
|
|
499
549
|
// Utility Methods
|
package/client/dist/types.d.ts
CHANGED
|
@@ -278,3 +278,62 @@ export interface DockerContainer {
|
|
|
278
278
|
canRestart: boolean;
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
282
|
+
export interface HttpTrafficEntry {
|
|
283
|
+
_id: string;
|
|
284
|
+
method: string;
|
|
285
|
+
targetUrl: string;
|
|
286
|
+
targetHost: string | null;
|
|
287
|
+
targetPath: string | null;
|
|
288
|
+
queryParams: string | null;
|
|
289
|
+
requestHeaders: string | null;
|
|
290
|
+
requestBody: string | null;
|
|
291
|
+
requestCookies: string | null;
|
|
292
|
+
contentType: string | null;
|
|
293
|
+
statusCode: number | null;
|
|
294
|
+
responseHeaders: string | null;
|
|
295
|
+
responseBody: string | null;
|
|
296
|
+
responseContentType: string | null;
|
|
297
|
+
responseTimeMs: number | null;
|
|
298
|
+
startedAt: string;
|
|
299
|
+
completedAt: string | null;
|
|
300
|
+
sourceService: string | null;
|
|
301
|
+
error: string | null;
|
|
302
|
+
traceId: string | null;
|
|
303
|
+
createdAt: string;
|
|
304
|
+
}
|
|
305
|
+
export interface HttpTrafficFilter {
|
|
306
|
+
method?: HttpMethod;
|
|
307
|
+
targetHost?: string;
|
|
308
|
+
sourceService?: string;
|
|
309
|
+
statusCode?: number;
|
|
310
|
+
traceId?: string;
|
|
311
|
+
since?: string;
|
|
312
|
+
until?: string;
|
|
313
|
+
}
|
|
314
|
+
export interface HttpTrafficWaitFilter {
|
|
315
|
+
method?: HttpMethod;
|
|
316
|
+
targetHost?: string;
|
|
317
|
+
sourceService?: string;
|
|
318
|
+
statusCode?: number;
|
|
319
|
+
traceId?: string;
|
|
320
|
+
pathContains?: string;
|
|
321
|
+
}
|
|
322
|
+
export interface HttpTrafficStats {
|
|
323
|
+
total: number;
|
|
324
|
+
errorCount: number;
|
|
325
|
+
errorRate: string;
|
|
326
|
+
avgResponseTimeMs: number;
|
|
327
|
+
byMethod: {
|
|
328
|
+
method: string;
|
|
329
|
+
count: number;
|
|
330
|
+
}[];
|
|
331
|
+
byHost: {
|
|
332
|
+
source_service: string | null;
|
|
333
|
+
count: number;
|
|
334
|
+
}[];
|
|
335
|
+
byStatus: {
|
|
336
|
+
status_code: number | null;
|
|
337
|
+
count: number;
|
|
338
|
+
}[];
|
|
339
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# App services (dev-tools backend + UI)
|
|
2
|
+
# Requires shared services running first: docker compose -f docker-compose.services.yml up -d
|
|
3
|
+
# Then start: docker compose -f docker-compose.dev.yml up -d
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
backend:
|
|
7
|
+
image: goki-dev-tools-backend
|
|
8
|
+
build:
|
|
9
|
+
context: .
|
|
10
|
+
dockerfile: Dockerfile.dev
|
|
11
|
+
args:
|
|
12
|
+
NPM_TOKEN: ${NPM_TOKEN}
|
|
13
|
+
container_name: goki-dev-tools-backend
|
|
14
|
+
ports:
|
|
15
|
+
- "9000:9000" # Backend API
|
|
16
|
+
- "8087:8087" # Cloud Logging Emulator
|
|
17
|
+
- "8883:8883" # MQTT Broker
|
|
18
|
+
environment:
|
|
19
|
+
- NODE_ENV=development
|
|
20
|
+
- WEB_UI_PORT=9000
|
|
21
|
+
# Shared services (use container names for cross-compose networking)
|
|
22
|
+
- POSTGRES_HOST=goki-postgres
|
|
23
|
+
- POSTGRES_PORT=5432
|
|
24
|
+
- POSTGRES_USER=postgres
|
|
25
|
+
- POSTGRES_PASSWORD=postgres
|
|
26
|
+
- POSTGRES_DATABASE=device_native
|
|
27
|
+
- REDIS_HOST=goki-redis
|
|
28
|
+
- REDIS_PORT=6379
|
|
29
|
+
- REDIS_LOGS_HOST=goki-redis-logs
|
|
30
|
+
- REDIS_LOGS_PORT=6380
|
|
31
|
+
- PUBSUB_EMULATOR_HOST=goki-pubsub-emulator:8085
|
|
32
|
+
- FIRESTORE_EMULATOR_HOST=goki-firestore-emulator:8080
|
|
33
|
+
- FIRESTORE_PROJECT_ID=tipi-development
|
|
34
|
+
- FIRESTORE_PROJECT_IDS=tipi-development,goki-dev-local
|
|
35
|
+
# App-specific config
|
|
36
|
+
- LOGGING_PORT=8087
|
|
37
|
+
- MQTT_PORT=8883
|
|
38
|
+
- DATA_DIR=/app/data
|
|
39
|
+
- AUTO_FLUSH_INTERVAL_MS=5000
|
|
40
|
+
- LOG_LEVEL=info
|
|
41
|
+
- UI_DEV_SERVER=http://goki-dev-tools-frontend:9001
|
|
42
|
+
- CHOKIDAR_USEPOLLING=true
|
|
43
|
+
- HOST_PROJECT_DIR=${HOST_PROJECT_DIR:-${PWD}}
|
|
44
|
+
volumes:
|
|
45
|
+
- ./src:/app/src:ro
|
|
46
|
+
- ./docs:/app/docs:ro
|
|
47
|
+
- ./data:/app/data
|
|
48
|
+
- ./logs:/app/logs
|
|
49
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
50
|
+
depends_on:
|
|
51
|
+
- frontend
|
|
52
|
+
networks:
|
|
53
|
+
- goki-network
|
|
54
|
+
restart: unless-stopped
|
|
55
|
+
|
|
56
|
+
frontend:
|
|
57
|
+
image: goki-dev-tools-frontend
|
|
58
|
+
build:
|
|
59
|
+
context: ./ui
|
|
60
|
+
dockerfile: Dockerfile.dev
|
|
61
|
+
container_name: goki-dev-tools-frontend
|
|
62
|
+
ports:
|
|
63
|
+
- "9001:9001" # React dev server
|
|
64
|
+
environment:
|
|
65
|
+
- PORT=9001
|
|
66
|
+
- CHOKIDAR_USEPOLLING=true
|
|
67
|
+
- WATCHPACK_POLLING=true
|
|
68
|
+
- WDS_SOCKET_PORT=9001
|
|
69
|
+
volumes:
|
|
70
|
+
- ./ui/src:/app/src
|
|
71
|
+
- ./ui/public:/app/public
|
|
72
|
+
- ./ui/tailwind.config.js:/app/tailwind.config.js:ro
|
|
73
|
+
- ./ui/postcss.config.js:/app/postcss.config.js:ro
|
|
74
|
+
networks:
|
|
75
|
+
- goki-network
|
|
76
|
+
restart: unless-stopped
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
networks:
|
|
80
|
+
goki-network:
|
|
81
|
+
external: true
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Shared infrastructure services (PostgreSQL, Redis, Emulators)
|
|
2
|
+
# Start these first: docker compose -f docker-compose.services.yml up -d
|
|
3
|
+
# Then start app services: docker compose -f docker-compose.dev.yml up -d
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
postgres:
|
|
7
|
+
image: postgres:15-alpine
|
|
8
|
+
container_name: goki-postgres
|
|
9
|
+
environment:
|
|
10
|
+
POSTGRES_USER: postgres
|
|
11
|
+
POSTGRES_PASSWORD: postgres
|
|
12
|
+
ports:
|
|
13
|
+
- "5432:5432"
|
|
14
|
+
volumes:
|
|
15
|
+
- postgres-data:/var/lib/postgresql/data
|
|
16
|
+
networks:
|
|
17
|
+
- goki-network
|
|
18
|
+
restart: unless-stopped
|
|
19
|
+
labels:
|
|
20
|
+
- "goki.service=postgres"
|
|
21
|
+
healthcheck:
|
|
22
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
23
|
+
interval: 5s
|
|
24
|
+
timeout: 5s
|
|
25
|
+
retries: 5
|
|
26
|
+
profiles:
|
|
27
|
+
- postgres
|
|
28
|
+
|
|
29
|
+
redis:
|
|
30
|
+
image: redis:7-alpine
|
|
31
|
+
container_name: goki-redis
|
|
32
|
+
ports:
|
|
33
|
+
- "6379:6379"
|
|
34
|
+
volumes:
|
|
35
|
+
- redis-data:/data
|
|
36
|
+
networks:
|
|
37
|
+
- goki-network
|
|
38
|
+
restart: unless-stopped
|
|
39
|
+
labels:
|
|
40
|
+
- "goki.service=redis"
|
|
41
|
+
healthcheck:
|
|
42
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
43
|
+
interval: 5s
|
|
44
|
+
timeout: 5s
|
|
45
|
+
retries: 5
|
|
46
|
+
|
|
47
|
+
redis-logs:
|
|
48
|
+
image: redis:7-alpine
|
|
49
|
+
container_name: goki-redis-logs
|
|
50
|
+
command: redis-server --port 6380 --maxmemory 256mb --maxmemory-policy allkeys-lru
|
|
51
|
+
ports:
|
|
52
|
+
- "6380:6380"
|
|
53
|
+
volumes:
|
|
54
|
+
- redis-logs-data:/data
|
|
55
|
+
networks:
|
|
56
|
+
- goki-network
|
|
57
|
+
restart: unless-stopped
|
|
58
|
+
labels:
|
|
59
|
+
- "goki.service=redis-logs"
|
|
60
|
+
healthcheck:
|
|
61
|
+
test: ["CMD", "redis-cli", "-p", "6380", "ping"]
|
|
62
|
+
interval: 5s
|
|
63
|
+
timeout: 5s
|
|
64
|
+
retries: 5
|
|
65
|
+
profiles:
|
|
66
|
+
- redis-logs
|
|
67
|
+
|
|
68
|
+
pubsub-emulator:
|
|
69
|
+
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
|
|
70
|
+
container_name: goki-pubsub-emulator
|
|
71
|
+
command: gcloud beta emulators pubsub start --host-port=0.0.0.0:8085 --project=tipi-development
|
|
72
|
+
ports:
|
|
73
|
+
- "8085:8085"
|
|
74
|
+
networks:
|
|
75
|
+
- goki-network
|
|
76
|
+
restart: unless-stopped
|
|
77
|
+
labels:
|
|
78
|
+
- "goki.service=pubsub-emulator"
|
|
79
|
+
healthcheck:
|
|
80
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:8085/v1/projects/tipi-development/topics || exit 1"]
|
|
81
|
+
interval: 5s
|
|
82
|
+
timeout: 5s
|
|
83
|
+
retries: 10
|
|
84
|
+
start_period: 10s
|
|
85
|
+
|
|
86
|
+
firestore-emulator:
|
|
87
|
+
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
|
|
88
|
+
container_name: goki-firestore-emulator
|
|
89
|
+
entrypoint: /bin/bash
|
|
90
|
+
command:
|
|
91
|
+
- -c
|
|
92
|
+
- |
|
|
93
|
+
IMPORT_FILE=$$(find /data/firestore-export -name '*.overall_export_metadata' 2>/dev/null | head -1)
|
|
94
|
+
if [ -n "$$IMPORT_FILE" ]; then
|
|
95
|
+
echo "Importing Firestore data from $$IMPORT_FILE"
|
|
96
|
+
exec gcloud emulators firestore start --host-port=0.0.0.0:8080 --project=goki-dev-local --export-on-exit=/data/firestore-export --import-data="$$IMPORT_FILE"
|
|
97
|
+
else
|
|
98
|
+
echo "No previous export found, starting fresh"
|
|
99
|
+
exec gcloud emulators firestore start --host-port=0.0.0.0:8080 --project=goki-dev-local --export-on-exit=/data/firestore-export
|
|
100
|
+
fi
|
|
101
|
+
ports:
|
|
102
|
+
- "8080:8080"
|
|
103
|
+
volumes:
|
|
104
|
+
- firestore-data:/data
|
|
105
|
+
networks:
|
|
106
|
+
- goki-network
|
|
107
|
+
restart: unless-stopped
|
|
108
|
+
labels:
|
|
109
|
+
- "goki.service=firestore-emulator"
|
|
110
|
+
healthcheck:
|
|
111
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:8080/ || exit 1"]
|
|
112
|
+
interval: 5s
|
|
113
|
+
timeout: 5s
|
|
114
|
+
retries: 10
|
|
115
|
+
start_period: 10s
|
|
116
|
+
profiles:
|
|
117
|
+
- firestore
|
|
118
|
+
|
|
119
|
+
elasticsearch:
|
|
120
|
+
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
|
|
121
|
+
container_name: goki-elasticsearch
|
|
122
|
+
environment:
|
|
123
|
+
- discovery.type=single-node
|
|
124
|
+
- xpack.security.enabled=false
|
|
125
|
+
- ES_JAVA_OPTS=-Xms1g -Xmx1g
|
|
126
|
+
ports:
|
|
127
|
+
- "9200:9200"
|
|
128
|
+
volumes:
|
|
129
|
+
- elasticsearch-data:/usr/share/elasticsearch/data
|
|
130
|
+
networks:
|
|
131
|
+
- goki-network
|
|
132
|
+
restart: unless-stopped
|
|
133
|
+
labels:
|
|
134
|
+
- "goki.service=elasticsearch"
|
|
135
|
+
deploy:
|
|
136
|
+
resources:
|
|
137
|
+
limits:
|
|
138
|
+
memory: 2g
|
|
139
|
+
healthcheck:
|
|
140
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
|
|
141
|
+
interval: 10s
|
|
142
|
+
timeout: 5s
|
|
143
|
+
retries: 10
|
|
144
|
+
start_period: 30s
|
|
145
|
+
profiles:
|
|
146
|
+
- elasticsearch
|
|
147
|
+
|
|
148
|
+
kibana:
|
|
149
|
+
image: docker.elastic.co/kibana/kibana:8.12.0
|
|
150
|
+
container_name: goki-kibana
|
|
151
|
+
environment:
|
|
152
|
+
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
|
|
153
|
+
ports:
|
|
154
|
+
- "5601:5601"
|
|
155
|
+
depends_on:
|
|
156
|
+
elasticsearch:
|
|
157
|
+
condition: service_healthy
|
|
158
|
+
networks:
|
|
159
|
+
- goki-network
|
|
160
|
+
restart: unless-stopped
|
|
161
|
+
labels:
|
|
162
|
+
- "goki.service=kibana"
|
|
163
|
+
deploy:
|
|
164
|
+
resources:
|
|
165
|
+
limits:
|
|
166
|
+
memory: 1g
|
|
167
|
+
healthcheck:
|
|
168
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:5601/api/status || exit 1"]
|
|
169
|
+
interval: 10s
|
|
170
|
+
timeout: 5s
|
|
171
|
+
retries: 10
|
|
172
|
+
start_period: 60s
|
|
173
|
+
profiles:
|
|
174
|
+
- kibana
|
|
175
|
+
|
|
176
|
+
networks:
|
|
177
|
+
goki-network:
|
|
178
|
+
name: goki-network
|
|
179
|
+
driver: bridge
|
|
180
|
+
|
|
181
|
+
volumes:
|
|
182
|
+
postgres-data:
|
|
183
|
+
redis-data:
|
|
184
|
+
redis-logs-data:
|
|
185
|
+
firestore-data:
|
|
186
|
+
elasticsearch-data:
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
services:
|
|
2
|
+
pubsub-emulator:
|
|
3
|
+
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
|
|
4
|
+
container_name: goki-pubsub-emulator
|
|
5
|
+
command: gcloud beta emulators pubsub start --host-port=0.0.0.0:8085 --project=tipi-development
|
|
6
|
+
ports:
|
|
7
|
+
- "8085:8085"
|
|
8
|
+
networks:
|
|
9
|
+
- goki-network
|
|
10
|
+
restart: unless-stopped
|
|
11
|
+
labels:
|
|
12
|
+
- "goki.service=pubsub-emulator"
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD", "curl", "-f", "http://localhost:8085"]
|
|
15
|
+
interval: 5s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 10
|
|
18
|
+
start_period: 10s
|
|
19
|
+
|
|
20
|
+
firestore-emulator:
|
|
21
|
+
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
|
|
22
|
+
container_name: goki-firestore-emulator
|
|
23
|
+
entrypoint: /bin/bash
|
|
24
|
+
command:
|
|
25
|
+
- -c
|
|
26
|
+
- |
|
|
27
|
+
IMPORT_FILE=$$(find /data/firestore-export -name '*.overall_export_metadata' 2>/dev/null | head -1)
|
|
28
|
+
if [ -n "$$IMPORT_FILE" ]; then
|
|
29
|
+
echo "Importing Firestore data from $$IMPORT_FILE"
|
|
30
|
+
exec gcloud emulators firestore start --host-port=0.0.0.0:8081 --project=goki-dev-local --export-on-exit=/data/firestore-export --import-data="$$IMPORT_FILE"
|
|
31
|
+
else
|
|
32
|
+
echo "No previous export found, starting fresh"
|
|
33
|
+
exec gcloud emulators firestore start --host-port=0.0.0.0:8081 --project=goki-dev-local --export-on-exit=/data/firestore-export
|
|
34
|
+
fi
|
|
35
|
+
ports:
|
|
36
|
+
- "8081:8081"
|
|
37
|
+
volumes:
|
|
38
|
+
- firestore-data:/data
|
|
39
|
+
networks:
|
|
40
|
+
- goki-network
|
|
41
|
+
restart: unless-stopped
|
|
42
|
+
labels:
|
|
43
|
+
- "goki.service=firestore-emulator"
|
|
44
|
+
healthcheck:
|
|
45
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:8081 || exit 1"]
|
|
46
|
+
interval: 10s
|
|
47
|
+
timeout: 10s
|
|
48
|
+
retries: 12
|
|
49
|
+
start_period: 60s
|
|
50
|
+
profiles:
|
|
51
|
+
- firestore
|
|
52
|
+
|
|
53
|
+
dev-tools-backend:
|
|
54
|
+
build:
|
|
55
|
+
context: .
|
|
56
|
+
dockerfile: Dockerfile.backend
|
|
57
|
+
args:
|
|
58
|
+
NPM_TOKEN: ${NPM_TOKEN}
|
|
59
|
+
container_name: goki-dev-tools-backend
|
|
60
|
+
ports:
|
|
61
|
+
- "9000:9000" # Backend API
|
|
62
|
+
- "8086:8086" # AWS IoT Core HTTPS API
|
|
63
|
+
- "8087:8087" # Cloud Logging Emulator
|
|
64
|
+
- "8883:8883" # MQTT Broker
|
|
65
|
+
labels:
|
|
66
|
+
- "goki.service=dev-tools-backend"
|
|
67
|
+
environment:
|
|
68
|
+
- NODE_ENV=production
|
|
69
|
+
- WEB_UI_PORT=9000
|
|
70
|
+
- PUBSUB_EMULATOR_HOST=pubsub-emulator:8085
|
|
71
|
+
- PUBSUB_PROJECT_ID=tipi-development
|
|
72
|
+
- SHADOW_POLL_INTERVAL_MS=200
|
|
73
|
+
- SHADOW_SUBSCRIPTION_CHECK_INTERVAL_MS=5000
|
|
74
|
+
- LOGGING_PORT=8087
|
|
75
|
+
- MQTT_PORT=8883
|
|
76
|
+
- AWS_IOT_PORT=8086
|
|
77
|
+
- FIRESTORE_EMULATOR_HOST=goki-firestore-emulator:8080
|
|
78
|
+
- FIRESTORE_PORT=8080
|
|
79
|
+
- FIRESTORE_PROJECT_ID=tipi-development
|
|
80
|
+
- DATA_DIR=/app/data
|
|
81
|
+
- AUTO_FLUSH_INTERVAL_MS=5000
|
|
82
|
+
- REDIS_HOST=redis
|
|
83
|
+
- REDIS_PORT=6379
|
|
84
|
+
- POSTGRES_HOST=postgres
|
|
85
|
+
- POSTGRES_PORT=5432
|
|
86
|
+
- POSTGRES_USER=postgres
|
|
87
|
+
- POSTGRES_PASSWORD=postgres
|
|
88
|
+
- LOG_LEVEL=info
|
|
89
|
+
# App Gateway config (use container names)
|
|
90
|
+
- APP_GATEWAY_PROPERTY_ID=550e8400-e29b-41d4-a716-446655440000
|
|
91
|
+
- APP_GATEWAY_DEVICE_NATIVE_URL=http://device-native-app:3000
|
|
92
|
+
- APP_GATEWAY_DEVICE_SIMULATOR_URL=http://device-simulator-app:3000
|
|
93
|
+
- APP_GATEWAY_SCAN_INTERVAL_SECONDS=30
|
|
94
|
+
- APP_GATEWAY_AUTO_START=true
|
|
95
|
+
volumes:
|
|
96
|
+
- ./data:/app/data
|
|
97
|
+
- ./logs:/app/logs
|
|
98
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
99
|
+
depends_on:
|
|
100
|
+
pubsub-emulator:
|
|
101
|
+
condition: service_healthy
|
|
102
|
+
redis:
|
|
103
|
+
condition: service_started
|
|
104
|
+
# Note: firestore and postgres are optional (have profiles)
|
|
105
|
+
# App should gracefully handle missing connections
|
|
106
|
+
networks:
|
|
107
|
+
- goki-network
|
|
108
|
+
restart: unless-stopped
|
|
109
|
+
|
|
110
|
+
dev-tools-frontend:
|
|
111
|
+
build:
|
|
112
|
+
context: .
|
|
113
|
+
dockerfile: Dockerfile.frontend
|
|
114
|
+
container_name: goki-dev-tools-frontend
|
|
115
|
+
ports:
|
|
116
|
+
- "9001:80" # Frontend UI
|
|
117
|
+
labels:
|
|
118
|
+
- "goki.service=dev-tools-frontend"
|
|
119
|
+
depends_on:
|
|
120
|
+
- dev-tools-backend
|
|
121
|
+
networks:
|
|
122
|
+
- goki-network
|
|
123
|
+
restart: unless-stopped
|
|
124
|
+
healthcheck:
|
|
125
|
+
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://127.0.0.1/health || exit 1"]
|
|
126
|
+
interval: 30s
|
|
127
|
+
timeout: 3s
|
|
128
|
+
start_period: 10s
|
|
129
|
+
retries: 3
|
|
130
|
+
|
|
131
|
+
redis:
|
|
132
|
+
image: redis:7-alpine
|
|
133
|
+
container_name: goki-redis
|
|
134
|
+
ports:
|
|
135
|
+
- "6379:6379"
|
|
136
|
+
volumes:
|
|
137
|
+
- redis-data:/data
|
|
138
|
+
networks:
|
|
139
|
+
- goki-network
|
|
140
|
+
restart: unless-stopped
|
|
141
|
+
labels:
|
|
142
|
+
- "goki.service=redis"
|
|
143
|
+
|
|
144
|
+
postgres:
|
|
145
|
+
image: postgres:15-alpine
|
|
146
|
+
container_name: goki-postgres
|
|
147
|
+
environment:
|
|
148
|
+
POSTGRES_USER: postgres
|
|
149
|
+
POSTGRES_PASSWORD: postgres
|
|
150
|
+
ports:
|
|
151
|
+
- "5432:5432"
|
|
152
|
+
volumes:
|
|
153
|
+
- postgres-data:/var/lib/postgresql/data
|
|
154
|
+
networks:
|
|
155
|
+
- goki-network
|
|
156
|
+
restart: unless-stopped
|
|
157
|
+
labels:
|
|
158
|
+
- "goki.service=postgres"
|
|
159
|
+
healthcheck:
|
|
160
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
161
|
+
interval: 5s
|
|
162
|
+
timeout: 5s
|
|
163
|
+
retries: 5
|
|
164
|
+
profiles:
|
|
165
|
+
- postgres
|
|
166
|
+
|
|
167
|
+
elasticsearch:
|
|
168
|
+
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
|
|
169
|
+
container_name: goki-elasticsearch
|
|
170
|
+
environment:
|
|
171
|
+
- discovery.type=single-node
|
|
172
|
+
- xpack.security.enabled=false
|
|
173
|
+
- ES_JAVA_OPTS=-Xms1g -Xmx1g
|
|
174
|
+
ports:
|
|
175
|
+
- "9200:9200"
|
|
176
|
+
volumes:
|
|
177
|
+
- elasticsearch-data:/usr/share/elasticsearch/data
|
|
178
|
+
networks:
|
|
179
|
+
- goki-network
|
|
180
|
+
restart: unless-stopped
|
|
181
|
+
labels:
|
|
182
|
+
- "goki.service=elasticsearch"
|
|
183
|
+
deploy:
|
|
184
|
+
resources:
|
|
185
|
+
limits:
|
|
186
|
+
memory: 2g
|
|
187
|
+
healthcheck:
|
|
188
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
|
|
189
|
+
interval: 10s
|
|
190
|
+
timeout: 5s
|
|
191
|
+
retries: 10
|
|
192
|
+
start_period: 30s
|
|
193
|
+
profiles:
|
|
194
|
+
- elasticsearch
|
|
195
|
+
|
|
196
|
+
kibana:
|
|
197
|
+
image: docker.elastic.co/kibana/kibana:8.12.0
|
|
198
|
+
container_name: goki-kibana
|
|
199
|
+
environment:
|
|
200
|
+
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
|
|
201
|
+
ports:
|
|
202
|
+
- "5601:5601"
|
|
203
|
+
depends_on:
|
|
204
|
+
elasticsearch:
|
|
205
|
+
condition: service_healthy
|
|
206
|
+
networks:
|
|
207
|
+
- goki-network
|
|
208
|
+
restart: unless-stopped
|
|
209
|
+
labels:
|
|
210
|
+
- "goki.service=kibana"
|
|
211
|
+
deploy:
|
|
212
|
+
resources:
|
|
213
|
+
limits:
|
|
214
|
+
memory: 1g
|
|
215
|
+
healthcheck:
|
|
216
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:5601/api/status || exit 1"]
|
|
217
|
+
interval: 10s
|
|
218
|
+
timeout: 5s
|
|
219
|
+
retries: 10
|
|
220
|
+
start_period: 60s
|
|
221
|
+
profiles:
|
|
222
|
+
- kibana
|
|
223
|
+
|
|
224
|
+
networks:
|
|
225
|
+
goki-network:
|
|
226
|
+
name: goki-network
|
|
227
|
+
driver: bridge
|
|
228
|
+
|
|
229
|
+
volumes:
|
|
230
|
+
redis-data:
|
|
231
|
+
postgres-data:
|
|
232
|
+
firestore-data:
|
|
233
|
+
elasticsearch-data:
|
package/nginx.conf
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
server {
|
|
2
|
+
listen 80;
|
|
3
|
+
server_name localhost;
|
|
4
|
+
root /usr/share/nginx/html;
|
|
5
|
+
index index.html;
|
|
6
|
+
|
|
7
|
+
# Gzip compression
|
|
8
|
+
gzip on;
|
|
9
|
+
gzip_vary on;
|
|
10
|
+
gzip_min_length 1024;
|
|
11
|
+
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
|
12
|
+
|
|
13
|
+
# Security headers
|
|
14
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
15
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
16
|
+
add_header X-XSS-Protection "1; mode=block" always;
|
|
17
|
+
|
|
18
|
+
# Serve static files
|
|
19
|
+
location / {
|
|
20
|
+
try_files $uri $uri/ /index.html;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Cache static assets
|
|
24
|
+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
25
|
+
expires 1y;
|
|
26
|
+
add_header Cache-Control "public, immutable";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Don't cache index.html
|
|
30
|
+
location = /index.html {
|
|
31
|
+
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
|
32
|
+
expires 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Health check endpoint
|
|
36
|
+
location /health {
|
|
37
|
+
access_log off;
|
|
38
|
+
return 200 "healthy\n";
|
|
39
|
+
add_header Content-Type text/plain;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gokiteam/goki-dev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Unified local development platform for Goki services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./client/dist/index.js",
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
"cli/",
|
|
30
30
|
"src/",
|
|
31
31
|
"client/dist/",
|
|
32
|
+
"ui/build/",
|
|
33
|
+
"docker-compose.yml",
|
|
34
|
+
"docker-compose.services.yml",
|
|
35
|
+
"docker-compose.dev.yml",
|
|
36
|
+
"Dockerfile.backend",
|
|
37
|
+
"Dockerfile.frontend",
|
|
38
|
+
"Dockerfile.dev",
|
|
39
|
+
"nginx.conf",
|
|
32
40
|
"config.development",
|
|
33
41
|
"config.test",
|
|
34
42
|
"guidelines/",
|
|
@@ -42,7 +50,10 @@
|
|
|
42
50
|
"ui:start": "cd ui && npm start",
|
|
43
51
|
"ui:build": "cd ui && npm run build",
|
|
44
52
|
"build:client": "cd client && npx tsc && echo '{\"type\":\"commonjs\"}' > dist/package.json",
|
|
45
|
-
"prepublishOnly": "npm run build:client",
|
|
53
|
+
"prepublishOnly": "npm run ui:build && npm run build:client",
|
|
54
|
+
"release": "npm version patch && npm run build:client && npm publish",
|
|
55
|
+
"release:minor": "npm version minor && npm run build:client && npm publish",
|
|
56
|
+
"release:major": "npm version major && npm run build:client && npm publish",
|
|
46
57
|
"test": "dotenv -e config.test mocha 'tests/**/*.test.js'",
|
|
47
58
|
"test:api": "dotenv -e config.test mocha 'tests/api/**/*.test.js'",
|
|
48
59
|
"test:emulation": "dotenv -e config.test mocha 'tests/emulation/**/*.test.js'",
|
|
@@ -15,6 +15,13 @@ export const Controllers = {
|
|
|
15
15
|
ctx.reply(result)
|
|
16
16
|
},
|
|
17
17
|
|
|
18
|
+
async waitFor (ctx) {
|
|
19
|
+
const { traceId } = ctx.state
|
|
20
|
+
const { filter, timeout } = ctx.request.body
|
|
21
|
+
const result = await Logic.waitForTraffic({ filter, timeout, traceId })
|
|
22
|
+
ctx.reply(result)
|
|
23
|
+
},
|
|
24
|
+
|
|
18
25
|
async clear (ctx) {
|
|
19
26
|
const { traceId } = ctx.state
|
|
20
27
|
const result = Logic.clear({ traceId })
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { HttpProxy } from '../../singletons/HttpProxy.js'
|
|
2
2
|
|
|
3
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
4
|
+
|
|
3
5
|
export const Logic = {
|
|
4
6
|
list (params) {
|
|
5
7
|
const { filter, limit = 50, offset = 0, traceId } = params
|
|
@@ -19,6 +21,32 @@ export const Logic = {
|
|
|
19
21
|
return { entry, traceId }
|
|
20
22
|
},
|
|
21
23
|
|
|
24
|
+
async waitForTraffic (params) {
|
|
25
|
+
const { filter, timeout = 5000, traceId } = params
|
|
26
|
+
const startTime = Date.now()
|
|
27
|
+
const pollInterval = 100
|
|
28
|
+
const { pathContains, ...equalityFilter } = filter
|
|
29
|
+
try {
|
|
30
|
+
while (Date.now() - startTime < timeout) {
|
|
31
|
+
const { data } = HttpProxy.listTraffic({ filter: equalityFilter, limit: 50 })
|
|
32
|
+
if (data && data.length > 0) {
|
|
33
|
+
for (const entry of data) {
|
|
34
|
+
if (!pathContains) {
|
|
35
|
+
return { entry, foundAt: Date.now() - startTime, traceId }
|
|
36
|
+
}
|
|
37
|
+
if (entry.targetPath && entry.targetPath.includes(pathContains)) {
|
|
38
|
+
return { entry, foundAt: Date.now() - startTime, traceId }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
await sleep(pollInterval)
|
|
43
|
+
}
|
|
44
|
+
return { status: 'error', message: 'Timeout waiting for HTTP traffic', traceId }
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { status: 'error', message: error.message, traceId }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
22
50
|
clear (params) {
|
|
23
51
|
const { traceId } = params
|
|
24
52
|
HttpProxy.clearTraffic()
|
|
@@ -5,5 +5,6 @@ export const Router = new KoaRouter({ prefix: '/v1/http-traffic' })
|
|
|
5
5
|
|
|
6
6
|
Router.post('/list', Controllers.list)
|
|
7
7
|
Router.post('/details', Controllers.details)
|
|
8
|
+
Router.post('/wait-for', Controllers.waitFor)
|
|
8
9
|
Router.post('/clear', Controllers.clear)
|
|
9
10
|
Router.post('/stats', Controllers.stats)
|
|
@@ -7,7 +7,9 @@ export const Schemas = {
|
|
|
7
7
|
targetHost: Joi.string(),
|
|
8
8
|
sourceService: Joi.string(),
|
|
9
9
|
statusCode: Joi.number().integer(),
|
|
10
|
-
traceId: Joi.string()
|
|
10
|
+
traceId: Joi.string(),
|
|
11
|
+
since: Joi.string().isoDate(),
|
|
12
|
+
until: Joi.string().isoDate()
|
|
11
13
|
}).optional(),
|
|
12
14
|
limit: Joi.number().integer().min(1).max(500).default(50),
|
|
13
15
|
offset: Joi.number().integer().min(0).default(0)
|
|
@@ -17,6 +19,18 @@ export const Schemas = {
|
|
|
17
19
|
id: Joi.string().required()
|
|
18
20
|
}),
|
|
19
21
|
|
|
22
|
+
waitFor: Joi.object({
|
|
23
|
+
filter: Joi.object({
|
|
24
|
+
method: Joi.string().valid('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'),
|
|
25
|
+
targetHost: Joi.string(),
|
|
26
|
+
sourceService: Joi.string(),
|
|
27
|
+
statusCode: Joi.number().integer(),
|
|
28
|
+
traceId: Joi.string(),
|
|
29
|
+
pathContains: Joi.string()
|
|
30
|
+
}).required(),
|
|
31
|
+
timeout: Joi.number().integer().min(100).max(30000).default(5000)
|
|
32
|
+
}),
|
|
33
|
+
|
|
20
34
|
clear: Joi.object({}),
|
|
21
35
|
|
|
22
36
|
stats: Joi.object({})
|
|
@@ -5,7 +5,7 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
5
5
|
'http_traffic_list',
|
|
6
6
|
'List recorded HTTP traffic between microservices (captured by the dev-tools proxy)',
|
|
7
7
|
{
|
|
8
|
-
filter: z.record(z.any()).optional().describe('Optional filter criteria (method, targetHost, sourceService, statusCode, traceId)'),
|
|
8
|
+
filter: z.record(z.any()).optional().describe('Optional filter criteria (method, targetHost, sourceService, statusCode, traceId, since, until)'),
|
|
9
9
|
limit: z.number().optional().describe('Maximum number of records to return'),
|
|
10
10
|
offset: z.number().optional().describe('Number of records to skip for pagination')
|
|
11
11
|
},
|
|
@@ -39,6 +39,25 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
39
39
|
}
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
server.tool(
|
|
43
|
+
'http_traffic_wait_for',
|
|
44
|
+
'Wait for an HTTP traffic entry matching the filter to appear within a timeout (polling). Useful for e2e tests.',
|
|
45
|
+
{
|
|
46
|
+
filter: z.record(z.any()).describe('Required filter criteria (method, targetHost, sourceService, statusCode, traceId, pathContains)'),
|
|
47
|
+
timeout: z.number().optional().describe('Timeout in ms (default: 5000, max: 30000)')
|
|
48
|
+
},
|
|
49
|
+
async ({ filter, timeout }) => {
|
|
50
|
+
try {
|
|
51
|
+
const body = { filter }
|
|
52
|
+
if (timeout !== undefined) body.timeout = timeout
|
|
53
|
+
const data = await apiClient.post('/v1/http-traffic/wait-for', body)
|
|
54
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return { content: [{ type: 'text', text: `Failed to wait for HTTP traffic: ${error.message}` }], isError: true }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
42
61
|
server.tool(
|
|
43
62
|
'http_traffic_clear',
|
|
44
63
|
'Clear all recorded HTTP traffic',
|
|
@@ -174,7 +174,17 @@ class HttpProxyClass {
|
|
|
174
174
|
offset
|
|
175
175
|
}
|
|
176
176
|
if (filter && Object.keys(filter).length > 0) {
|
|
177
|
-
|
|
177
|
+
const { since, until, ...equalityFilter } = filter
|
|
178
|
+
if (Object.keys(equalityFilter).length > 0) {
|
|
179
|
+
listOptions.filter = equalityFilter
|
|
180
|
+
}
|
|
181
|
+
const conditions = []
|
|
182
|
+
const params = []
|
|
183
|
+
if (since) { conditions.push('created_at >= ?'); params.push(since) }
|
|
184
|
+
if (until) { conditions.push('created_at <= ?'); params.push(until) }
|
|
185
|
+
if (conditions.length > 0) {
|
|
186
|
+
listOptions.rawWhere = { sql: conditions.join(' AND '), params }
|
|
187
|
+
}
|
|
178
188
|
}
|
|
179
189
|
return SqliteStore.list(HTTP_TRAFFIC, listOptions)
|
|
180
190
|
}
|
|
@@ -147,8 +147,12 @@ class SqliteStoreClass {
|
|
|
147
147
|
params.push(...where.params)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
if (options.rawWhere) {
|
|
151
|
+
sql += params.length > 0 ? ` AND ${options.rawWhere.sql}` : ` WHERE ${options.rawWhere.sql}`
|
|
152
|
+
params.push(...options.rawWhere.params)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const countSql = sql.replace(/^SELECT \* FROM/, 'SELECT COUNT(*) as count FROM')
|
|
152
156
|
const totalResult = this.db.prepare(countSql).get(...params)
|
|
153
157
|
const total = totalResult.count
|
|
154
158
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Copy package files
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
|
|
8
|
+
# Install dependencies
|
|
9
|
+
RUN npm install
|
|
10
|
+
|
|
11
|
+
# Copy config files (source files will be mounted as volumes)
|
|
12
|
+
COPY tailwind.config.js ./
|
|
13
|
+
COPY postcss.config.js ./
|
|
14
|
+
|
|
15
|
+
# Expose React dev server port
|
|
16
|
+
EXPOSE 9001
|
|
17
|
+
|
|
18
|
+
# Start React dev server
|
|
19
|
+
CMD ["npm", "start"]
|