@malloy-publisher/server 0.0.165 → 0.0.167
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/dist/app/api-doc.yaml +107 -0
- package/dist/app/assets/{HomePage-QekMXs8r.js → HomePage-D76UaGFV.js} +1 -1
- package/dist/app/assets/{MainPage-DAyUfYba.js → MainPage-C9Fr5IN8.js} +1 -1
- package/dist/app/assets/{ModelPage-CrMryV1s.js → ModelPage-BkU6HAHA.js} +1 -1
- package/dist/app/assets/{PackagePage-DDaABD2A.js → PackagePage-BhE9Wi7b.js} +1 -1
- package/dist/app/assets/{ProjectPage-FAYUFGhL.js → ProjectPage-BatZLVap.js} +1 -1
- package/dist/app/assets/{RouteError-BKYctANX.js → RouteError-Bo5zJ8Xa.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DZEVYGW3.js → WorkbookPage-D3rUQZj6.js} +1 -1
- package/dist/app/assets/{index-BvVmB5sv.js → index-BLxl0XLH.js} +71 -71
- package/dist/app/assets/{index-DWhjtyBB.js → index-hkABoiMV.js} +1 -1
- package/dist/app/assets/{index-CsC07BYd.js → index-lhDwptrQ.js} +1 -1
- package/dist/app/assets/{index.umd-DvM-lTQa.js → index.umd-BkXQ-YAe.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.js +85955 -88560
- package/dist/server.js +197162 -106231
- package/package.json +2 -1
- package/src/controller/compile.controller.ts +35 -0
- package/src/controller/model.controller.ts +20 -9
- package/src/health.ts +8 -0
- package/src/instrumentation.ts +123 -34
- package/src/server.ts +44 -2
- package/src/service/connection.spec.ts +1226 -0
- package/src/service/connection.ts +114 -12
- package/src/service/db_utils.ts +19 -41
- package/src/service/gcs_s3_utils.ts +115 -40
- package/src/service/model.ts +5 -5
- package/src/service/project.ts +120 -1
- package/src/service/project_compile.spec.ts +197 -0
- package/src/service/project_store.ts +49 -21
- package/src/storage/StorageManager.ts +4 -3
- package/src/storage/duckdb/schema.ts +6 -5
- package/tests/harness/e2e.ts +4 -0
- package/tests/harness/mcp_test_setup.ts +6 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.167",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.js"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
43
43
|
"@opentelemetry/api": "^1.9.0",
|
|
44
44
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
45
|
+
"@opentelemetry/exporter-prometheus": "^0.212.0",
|
|
45
46
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
46
47
|
"@opentelemetry/sdk-node": "^0.200.0",
|
|
47
48
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LogMessage } from "@malloydata/malloy";
|
|
2
|
+
import { ProjectStore } from "../service/project_store";
|
|
3
|
+
|
|
4
|
+
export class CompileController {
|
|
5
|
+
private projectStore: ProjectStore;
|
|
6
|
+
|
|
7
|
+
constructor(projectStore: ProjectStore) {
|
|
8
|
+
this.projectStore = projectStore;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public async compile(
|
|
12
|
+
projectName: string,
|
|
13
|
+
packageName: string,
|
|
14
|
+
modelName: string,
|
|
15
|
+
source: string,
|
|
16
|
+
includeSql: boolean = false,
|
|
17
|
+
): Promise<{ status: string; problems: LogMessage[]; sql?: string }> {
|
|
18
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
19
|
+
const { problems, sql } = await project.compileSource(
|
|
20
|
+
packageName,
|
|
21
|
+
modelName,
|
|
22
|
+
source,
|
|
23
|
+
includeSql,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Determine overall status based on presence of errors
|
|
27
|
+
const hasErrors = problems.some((p) => p.severity === "error");
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
status: hasErrors ? "error" : "success",
|
|
31
|
+
problems: problems,
|
|
32
|
+
...(sql !== undefined && { sql }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -38,16 +38,27 @@ export class ModelController {
|
|
|
38
38
|
packageName: string,
|
|
39
39
|
modelPath: string,
|
|
40
40
|
): Promise<ApiCompiledModel> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
try {
|
|
42
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
43
|
+
const p = await project.getPackage(packageName, false);
|
|
44
|
+
const model = p.getModel(modelPath);
|
|
45
|
+
if (!model) {
|
|
46
|
+
throw new ModelNotFoundError(`${modelPath} does not exist`);
|
|
47
|
+
}
|
|
48
|
+
if (model.getType() === "notebook") {
|
|
49
|
+
throw new ModelNotFoundError(`${modelPath} is a notebook`);
|
|
50
|
+
}
|
|
51
|
+
return await model.getModel();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Re-throw ModelNotFoundError as-is
|
|
54
|
+
if (error instanceof ModelNotFoundError) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
// Wrap other errors with more context
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Failed to get model ${modelPath} from package ${packageName} in project ${projectName}: ${error}`,
|
|
60
|
+
);
|
|
49
61
|
}
|
|
50
|
-
return model.getModel();
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
public async getNotebook(
|
package/src/health.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { Express, NextFunction, Request, Response } from "express";
|
|
14
14
|
import { Server } from "http";
|
|
15
15
|
import { components } from "./api";
|
|
16
|
+
import { shutdownSDK } from "./instrumentation";
|
|
16
17
|
import { logger } from "./logger";
|
|
17
18
|
export type OperationalState =
|
|
18
19
|
components["schemas"]["ServerStatus"]["operationalState"];
|
|
@@ -112,6 +113,13 @@ export function registerSignalHandlers(
|
|
|
112
113
|
closeServer(mcpServer, "MCP server"),
|
|
113
114
|
]);
|
|
114
115
|
|
|
116
|
+
try {
|
|
117
|
+
await shutdownSDK();
|
|
118
|
+
logger.info("OpenTelemetry SDK shut down");
|
|
119
|
+
} catch (_error) {
|
|
120
|
+
/* do nothing */
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
try {
|
|
116
124
|
logger.close();
|
|
117
125
|
} catch (_error) {
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,59 +1,148 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
3
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
4
|
-
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
1
|
+
import { metrics } from "@opentelemetry/api";
|
|
5
2
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
3
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
4
|
+
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
|
5
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
6
|
+
import {
|
|
7
|
+
ExpressInstrumentation,
|
|
8
|
+
ExpressLayerType,
|
|
9
|
+
} from "@opentelemetry/instrumentation-express";
|
|
6
10
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
11
|
+
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
12
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
13
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
7
14
|
import {
|
|
8
15
|
ATTR_SERVICE_NAME,
|
|
9
16
|
ATTR_SERVICE_VERSION,
|
|
10
17
|
} from "@opentelemetry/semantic-conventions";
|
|
11
|
-
import {
|
|
12
|
-
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
13
|
-
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
18
|
+
import type { NextFunction, Request, Response } from "express";
|
|
14
19
|
import { logger } from "./logger";
|
|
15
20
|
|
|
21
|
+
let prometheusExporter: PrometheusExporter | null = null;
|
|
22
|
+
let sdk: NodeSDK | null = null;
|
|
23
|
+
|
|
24
|
+
export function getPrometheusMetricsHandler() {
|
|
25
|
+
if (!prometheusExporter) {
|
|
26
|
+
throw new Error("Prometheus exporter not initialized");
|
|
27
|
+
}
|
|
28
|
+
return (req: Request, res: Response) => {
|
|
29
|
+
prometheusExporter!.getMetricsRequestHandler(req, res);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shuts down the OpenTelemetry SDK gracefully.
|
|
35
|
+
* Should be called during application shutdown.
|
|
36
|
+
*/
|
|
37
|
+
export async function shutdownSDK(): Promise<void> {
|
|
38
|
+
if (sdk) {
|
|
39
|
+
await sdk.shutdown();
|
|
40
|
+
sdk = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
function instrument() {
|
|
17
45
|
const otelCollectorUrl = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
18
|
-
if (!otelCollectorUrl) {
|
|
19
|
-
logger.info("No OTLP collector URL found, skipping instrumentation");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
const traceExporter = new OTLPTraceExporter({
|
|
27
|
-
url: `${otelCollectorUrl}/v1/traces`,
|
|
28
|
-
headers: {},
|
|
29
|
-
});
|
|
30
|
-
const metricExporter = new OTLPMetricExporter({
|
|
31
|
-
url: `${otelCollectorUrl}/v1/metrics`,
|
|
32
|
-
headers: {},
|
|
33
|
-
});
|
|
34
|
-
const logExporter = new OTLPLogExporter({
|
|
35
|
-
url: `${otelCollectorUrl}/v1/logs`,
|
|
36
|
-
headers: {},
|
|
47
|
+
prometheusExporter = new PrometheusExporter({
|
|
48
|
+
preventServerStart: true,
|
|
37
49
|
});
|
|
38
50
|
|
|
39
|
-
const
|
|
40
|
-
|
|
51
|
+
const instrumentations = [
|
|
52
|
+
getNodeAutoInstrumentations(),
|
|
53
|
+
new ExpressInstrumentation({
|
|
54
|
+
ignoreLayersType: [ExpressLayerType.MIDDLEWARE],
|
|
55
|
+
ignoreLayers: [/\/health/, /\/metrics/],
|
|
56
|
+
}),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
sdk = new NodeSDK({
|
|
41
60
|
resource: resourceFromAttributes({
|
|
42
61
|
[ATTR_SERVICE_NAME]: "publisher",
|
|
43
62
|
[ATTR_SERVICE_VERSION]: "1.0.0",
|
|
44
63
|
}),
|
|
45
64
|
autoDetectResources: true,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
metricReader: prometheusExporter,
|
|
66
|
+
instrumentations,
|
|
67
|
+
...(otelCollectorUrl && {
|
|
68
|
+
spanProcessors: [
|
|
69
|
+
new BatchSpanProcessor(
|
|
70
|
+
new OTLPTraceExporter({
|
|
71
|
+
url: `${otelCollectorUrl}/v1/traces`,
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
logRecordProcessors: [
|
|
76
|
+
new BatchLogRecordProcessor(
|
|
77
|
+
new OTLPLogExporter({
|
|
78
|
+
url: `${otelCollectorUrl}/v1/logs`,
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
],
|
|
49
82
|
}),
|
|
50
|
-
instrumentations: [getNodeAutoInstrumentations()],
|
|
51
|
-
spanProcessors: [new BatchSpanProcessor(traceExporter)],
|
|
52
|
-
logRecordProcessors: [new BatchLogRecordProcessor(logExporter)],
|
|
53
83
|
});
|
|
54
84
|
|
|
55
85
|
sdk.start();
|
|
56
|
-
|
|
86
|
+
|
|
87
|
+
if (otelCollectorUrl) {
|
|
88
|
+
logger.info(
|
|
89
|
+
`OpenTelemetry SDK initialized with OTLP collector at ${otelCollectorUrl}`,
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
logger.info("OpenTelemetry SDK initialized with Prometheus metrics only");
|
|
93
|
+
}
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
instrument();
|
|
97
|
+
|
|
98
|
+
// --- HTTP metrics middleware ---
|
|
99
|
+
|
|
100
|
+
const meter = metrics.getMeter("publisher");
|
|
101
|
+
|
|
102
|
+
const httpRequestDuration = meter.createHistogram(
|
|
103
|
+
"http_server_request_duration_ms",
|
|
104
|
+
{
|
|
105
|
+
description: "Duration of HTTP requests in milliseconds",
|
|
106
|
+
unit: "ms",
|
|
107
|
+
advice: {
|
|
108
|
+
explicitBucketBoundaries: [
|
|
109
|
+
5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 60000,
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const httpRequestCount = meter.createCounter("http_server_requests_total", {
|
|
116
|
+
description: "Total number of HTTP requests",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const IGNORED_PATHS = new Set([
|
|
120
|
+
"/health",
|
|
121
|
+
"/health/liveness",
|
|
122
|
+
"/health/readiness",
|
|
123
|
+
"/metrics",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
export function httpMetricsMiddleware(
|
|
127
|
+
req: Request,
|
|
128
|
+
res: Response,
|
|
129
|
+
next: NextFunction,
|
|
130
|
+
) {
|
|
131
|
+
const start = performance.now();
|
|
132
|
+
|
|
133
|
+
res.on("finish", () => {
|
|
134
|
+
if (IGNORED_PATHS.has(req.path)) return;
|
|
135
|
+
|
|
136
|
+
const duration = performance.now() - start;
|
|
137
|
+
const attrs = {
|
|
138
|
+
"http.method": req.method,
|
|
139
|
+
"http.route": req.route?.path ?? req.path,
|
|
140
|
+
"http.status_code": res.statusCode,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
httpRequestDuration.record(duration, attrs);
|
|
144
|
+
httpRequestCount.add(1, attrs);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
next();
|
|
148
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Pre-load the instrumentation module; the instrumentation module must be loaded before the other imports.
|
|
2
|
+
import "./instrumentation";
|
|
3
|
+
import {
|
|
4
|
+
getPrometheusMetricsHandler,
|
|
5
|
+
httpMetricsMiddleware,
|
|
6
|
+
} from "./instrumentation";
|
|
7
|
+
|
|
1
8
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
9
|
import * as bodyParser from "body-parser";
|
|
3
10
|
import cors from "cors";
|
|
@@ -6,6 +13,7 @@ import * as http from "http";
|
|
|
6
13
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
7
14
|
import { AddressInfo } from "net";
|
|
8
15
|
import * as path from "path";
|
|
16
|
+
import { CompileController } from "./controller/compile.controller";
|
|
9
17
|
import { ConnectionController } from "./controller/connection.controller";
|
|
10
18
|
import { DatabaseController } from "./controller/database.controller";
|
|
11
19
|
import { ModelController } from "./controller/model.controller";
|
|
@@ -19,6 +27,7 @@ import {
|
|
|
19
27
|
registerSignalHandlers,
|
|
20
28
|
} from "./health";
|
|
21
29
|
import { logger, loggerMiddleware } from "./logger";
|
|
30
|
+
|
|
22
31
|
import { initializeMcpServer } from "./mcp/server";
|
|
23
32
|
import { ProjectStore } from "./service/project_store";
|
|
24
33
|
// Parse command line arguments
|
|
@@ -110,7 +119,7 @@ const isDevelopment = process.env["NODE_ENV"] === "development";
|
|
|
110
119
|
|
|
111
120
|
const app = express();
|
|
112
121
|
app.use(loggerMiddleware);
|
|
113
|
-
|
|
122
|
+
app.use(httpMetricsMiddleware);
|
|
114
123
|
const projectStore = new ProjectStore(SERVER_ROOT);
|
|
115
124
|
const watchModeController = new WatchModeController(projectStore);
|
|
116
125
|
const connectionController = new ConnectionController(projectStore);
|
|
@@ -118,6 +127,7 @@ const modelController = new ModelController(projectStore);
|
|
|
118
127
|
const packageController = new PackageController(projectStore);
|
|
119
128
|
const databaseController = new DatabaseController(projectStore);
|
|
120
129
|
const queryController = new QueryController(projectStore);
|
|
130
|
+
const compileController = new CompileController(projectStore);
|
|
121
131
|
|
|
122
132
|
export const mcpApp = express();
|
|
123
133
|
|
|
@@ -220,7 +230,10 @@ if (!isDevelopment) {
|
|
|
220
230
|
target: "http://localhost:5173",
|
|
221
231
|
changeOrigin: true,
|
|
222
232
|
ws: true,
|
|
223
|
-
pathFilter: (path) =>
|
|
233
|
+
pathFilter: (path) =>
|
|
234
|
+
!path.startsWith("/api/") &&
|
|
235
|
+
!path.startsWith("/metrics") &&
|
|
236
|
+
!path.startsWith("/health"),
|
|
224
237
|
}),
|
|
225
238
|
);
|
|
226
239
|
}
|
|
@@ -243,6 +256,15 @@ app.use(bodyParser.json());
|
|
|
243
256
|
// Register health check endpoints
|
|
244
257
|
registerHealthEndpoints(app);
|
|
245
258
|
|
|
259
|
+
// Register Prometheus metrics endpoint
|
|
260
|
+
try {
|
|
261
|
+
const metricsHandler = getPrometheusMetricsHandler();
|
|
262
|
+
app.get("/metrics", metricsHandler);
|
|
263
|
+
logger.info("Prometheus metrics endpoint registered at /metrics");
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.warn("Failed to register Prometheus metrics endpoint", { error });
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
// Register draining guard middleware - must be after health endpoints but before other routes
|
|
247
269
|
app.use(drainingGuard);
|
|
248
270
|
|
|
@@ -897,6 +919,26 @@ app.get(
|
|
|
897
919
|
},
|
|
898
920
|
);
|
|
899
921
|
|
|
922
|
+
app.post(
|
|
923
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/models/:modelName/compile`,
|
|
924
|
+
async (req, res) => {
|
|
925
|
+
try {
|
|
926
|
+
const result = await compileController.compile(
|
|
927
|
+
req.params.projectName,
|
|
928
|
+
req.params.packageName,
|
|
929
|
+
req.params.modelName,
|
|
930
|
+
req.body.source,
|
|
931
|
+
req.body.includeSql === true,
|
|
932
|
+
);
|
|
933
|
+
res.status(200).json(result);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
logger.error("Compilation error", { error });
|
|
936
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
937
|
+
res.status(status).json(json);
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
|
|
900
942
|
// Modify the catch-all route to only serve index.html in production
|
|
901
943
|
if (!isDevelopment) {
|
|
902
944
|
app.get("*", (_req, res) => res.sendFile(path.resolve(ROOT, "index.html")));
|