diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.test.ts b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.test.ts index fe898f94d90bd..a2dc1994abf23 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.test.ts +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' import { formatDuration } from './QueryPerformance.utils' +import { calculatePercentilesFromHistogram } from './WithMonitor/WithMonitor.utils' describe('formatDuration', () => { it('should format seconds', () => { @@ -22,3 +23,23 @@ describe('formatDuration', () => { expect(formatDuration(90061000)).toBe('1d 1h 1m 1s') }) }) + +describe('calculatePercentilesFromHistogram', () => { + it('should return zero for empty histogram', () => { + const result = calculatePercentilesFromHistogram([]) + expect(result.p95).toBe(0) + }) + + it('should return valid p95 for typical distribution', () => { + const result = calculatePercentilesFromHistogram([10, 20, 30, 20, 10, 10]) + expect(result.p95).toBeGreaterThan(0) + expect(result.p95).toBeGreaterThanOrEqual(result.p50) + }) + + it('should return consistent p95 for same input', () => { + const histogram = [10, 20, 30, 20, 10, 10] + const result1 = calculatePercentilesFromHistogram(histogram) + const result2 = calculatePercentilesFromHistogram(histogram) + expect(result1.p95).toBe(result2.p95) + }) +}) diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx index 30fa2578e1f65..a8672d4ac2a85 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx @@ -5,6 +5,7 @@ import { Loader2 } from 'lucide-react' import { ComposedChart } from 'components/ui/Charts/ComposedChart' import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils' import type { ChartDataPoint } from './WithMonitor/WithMonitor.utils' +import { calculatePercentilesFromHistogram } from './WithMonitor/WithMonitor.utils' interface QueryPerformanceChartProps { dateRange?: { @@ -61,12 +62,38 @@ export const QueryPerformanceChart = ({ switch (selectedMetric) { case 'query_latency': { - const avgP95 = chartData.reduce((sum, d) => sum + d.p95_time, 0) / chartData.length + let trueP95: number = 0 + + if (parsedLogs && parsedLogs.length > 0) { + const bucketCount = parsedLogs[0]?.resp_calls?.length || 50 + const combinedHistogram = new Array(bucketCount).fill(0) + + parsedLogs.forEach((log) => { + if (log.resp_calls && Array.isArray(log.resp_calls)) { + log.resp_calls.forEach((count: number, index: number) => { + if (index < combinedHistogram.length) { + combinedHistogram[index] += count + } + }) + } + }) + + // [kemal]: this might need a revisit + const percentiles = calculatePercentilesFromHistogram(combinedHistogram) + trueP95 = percentiles.p95 + } else { + // [kemal]: fallback to weighted average + const totalCalls = chartData.reduce((sum, d) => sum + d.calls, 0) + trueP95 = + totalCalls > 0 + ? chartData.reduce((sum, d) => sum + d.p95_time * d.calls, 0) / totalCalls + : 0 + } return [ { label: 'Average p95', - value: avgP95 >= 100 ? `${(avgP95 / 1000).toFixed(2)}s` : `${Math.round(avgP95)}ms`, + value: `${Math.round(trueP95)}ms`, }, ] } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 536ea8860d5bc..5d9b3e82fb940 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -71,7 +71,7 @@ services: KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml # https://github.com/supabase/cli/issues/14 KONG_DNS_ORDER: LAST,A,CNAME - KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k SUPABASE_ANON_KEY: ${ANON_KEY} diff --git a/docker/volumes/api/kong.yml b/docker/volumes/api/kong.yml index 7abf42534c917..168634f5a9a40 100644 --- a/docker/volumes/api/kong.yml +++ b/docker/volumes/api/kong.yml @@ -225,6 +225,48 @@ services: allow: - admin + ## Block access to /api/mcp + - name: mcp-blocker + _comment: 'Block direct access to /api/mcp' + url: http://studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + ## MCP endpoint - local access + - name: mcp + _comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)' + url: http://studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + paths: + - /mcp + plugins: + # Block access to /mcp by default + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + # Enable local access (danger zone!) + # 1. Comment out the 'request-termination' section above + # 2. Uncomment the entire section below, including 'deny' + # 3. Add your local IPs to the 'allow' list + #- name: cors + #- name: ip-restriction + # config: + # allow: + # - 127.0.0.1 + # - ::1 + # deny: [] + ## Protected Dashboard - catch all remaining routes - name: dashboard _comment: 'Studio: /* -> http://studio:3000/*'