feat: http logging middleware + mcp random port (#74)
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kandev/kandev/internal/common/httpmw"
|
||||
"go.uber.org/zap"
|
||||
|
||||
// Common packages
|
||||
@@ -648,6 +649,7 @@ func main() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
router := gin.New()
|
||||
router.Use(httpmw.RequestLogger(log, "kandev"))
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(corsMiddleware())
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/instance"
|
||||
"github.com/kandev/kandev/internal/common/httpmw"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -33,6 +34,8 @@ func NewControlServer(cfg *config.Config, instMgr *instance.Manager, log *logger
|
||||
router: gin.New(),
|
||||
}
|
||||
|
||||
cs.router.Use(httpmw.RequestLogger(cs.logger, "agentctl-control"))
|
||||
|
||||
cs.setupRoutes()
|
||||
return cs
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/process"
|
||||
"github.com/kandev/kandev/internal/common/httpmw"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -39,6 +40,8 @@ func NewServer(cfg *config.InstanceConfig, procMgr *process.Manager, log *logger
|
||||
},
|
||||
}
|
||||
|
||||
s.router.Use(httpmw.RequestLogger(s.logger, "agentctl-instance"))
|
||||
|
||||
s.setupRoutes()
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -212,6 +212,8 @@ func LoadWithPath(configPath string) (*Config, error) {
|
||||
// so we explicitly bind keys where env var naming differs from config key naming.
|
||||
_ = v.BindEnv("agent.standalonePort", "AGENTCTL_PORT", "KANDEV_AGENT_STANDALONE_PORT")
|
||||
_ = v.BindEnv("agent.standaloneHost", "KANDEV_AGENT_STANDALONE_HOST")
|
||||
_ = v.BindEnv("agent.mcpServerPort", "KANDEV_AGENT_MCP_SERVER_PORT")
|
||||
_ = v.BindEnv("agent.mcpServerUrl", "KANDEV_AGENT_MCP_SERVER_URL")
|
||||
|
||||
// Configure config file
|
||||
v.SetConfigName("config")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RequestLogger logs HTTP request details after the handler completes.
|
||||
func RequestLogger(log *logger.Logger, serverName string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
status := c.Writer.Status()
|
||||
size := c.Writer.Size()
|
||||
if size < 0 {
|
||||
size = 0
|
||||
}
|
||||
|
||||
log.Info("http",
|
||||
zap.String("server", serverName),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", path),
|
||||
zap.Int("status", status),
|
||||
zap.Int64("duration_ms", latency.Milliseconds()),
|
||||
zap.Int("bytes", size),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,9 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
||||
}
|
||||
if tcpAddr, ok := listener.Addr().(*net.TCPAddr); ok {
|
||||
s.cfg.Port = tcpAddr.Port
|
||||
}
|
||||
|
||||
// Create HTTP server with the mux
|
||||
s.httpServer = &http.Server{
|
||||
@@ -174,4 +177,3 @@ func (s *Server) SSEEndpoint() string {
|
||||
func (s *Server) StreamableHTTPEndpoint() string {
|
||||
return fmt.Sprintf("http://localhost:%d/mcp", s.cfg.Port)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
export const DEFAULT_BACKEND_PORT = 8080;
|
||||
export const DEFAULT_WEB_PORT = 3000;
|
||||
export const DEFAULT_AGENTCTL_PORT = 9999;
|
||||
export const DEFAULT_MCP_PORT = 9090;
|
||||
|
||||
// Random fallback range for port selection.
|
||||
export const RANDOM_PORT_MIN = 10000;
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import {
|
||||
DEFAULT_AGENTCTL_PORT,
|
||||
DEFAULT_BACKEND_PORT,
|
||||
DEFAULT_MCP_PORT,
|
||||
DEFAULT_WEB_PORT,
|
||||
HEALTH_TIMEOUT_MS,
|
||||
} from "./constants";
|
||||
@@ -22,17 +23,22 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr
|
||||
const actualBackendPort = backendPort ?? (await pickAvailablePort(DEFAULT_BACKEND_PORT));
|
||||
const actualWebPort = webPort ?? (await pickAvailablePort(DEFAULT_WEB_PORT));
|
||||
const agentctlPort = await pickAvailablePort(DEFAULT_AGENTCTL_PORT);
|
||||
const mcpPort = await pickAvailablePort(DEFAULT_MCP_PORT);
|
||||
const backendUrl = `http://localhost:${actualBackendPort}`;
|
||||
const mcpUrl = `http://localhost:${mcpPort}/sse`;
|
||||
|
||||
const backendEnv = {
|
||||
...process.env,
|
||||
KANDEV_SERVER_PORT: String(actualBackendPort),
|
||||
KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort),
|
||||
KANDEV_AGENT_MCP_SERVER_PORT: String(mcpPort),
|
||||
};
|
||||
const webEnv = {
|
||||
...process.env,
|
||||
KANDEV_API_BASE_URL: backendUrl,
|
||||
NEXT_PUBLIC_KANDEV_API_BASE_URL: backendUrl,
|
||||
KANDEV_MCP_SERVER_URL: mcpUrl,
|
||||
NEXT_PUBLIC_KANDEV_MCP_SERVER_URL: mcpUrl,
|
||||
PORT: String(actualWebPort),
|
||||
NEXT_PUBLIC_KANDEV_DEBUG: "true",
|
||||
};
|
||||
@@ -41,6 +47,8 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr
|
||||
console.log("[kandev] backend port:", actualBackendPort);
|
||||
console.log("[kandev] web port:", actualWebPort);
|
||||
console.log("[kandev] agentctl port:", agentctlPort);
|
||||
console.log("[kandev] mcp port:", mcpPort);
|
||||
console.log("[kandev] mcp url:", mcpUrl);
|
||||
|
||||
const supervisor = createProcessSupervisor();
|
||||
supervisor.attachSignalHandlers();
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const runtimeApiBaseUrl = process.env.KANDEV_API_BASE_URL ?? "";
|
||||
const runtimeMcpServerUrl = process.env.KANDEV_MCP_SERVER_URL ?? "";
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="antialiased font-sans">
|
||||
@@ -28,6 +29,13 @@ export default function RootLayout({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{runtimeMcpServerUrl ? (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.__KANDEV_MCP_SERVER_URL = ${JSON.stringify(runtimeMcpServerUrl)};`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<StateProvider>
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import { Switch } from '@kandev/ui/switch';
|
||||
import { Textarea } from '@kandev/ui/textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
|
||||
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
|
||||
import { getBackendConfig } from '@/lib/config';
|
||||
import { useProfileMcpConfig } from './use-profile-mcp-config';
|
||||
import type { AgentProfileMcpConfig } from '@/lib/types/http';
|
||||
|
||||
@@ -62,11 +64,13 @@ export function ProfileMcpConfigCard({
|
||||
const currentServers = isDraft ? draftState?.servers ?? '' : mcpServers;
|
||||
const currentError = isDraft ? draftState?.error ?? null : mcpError;
|
||||
const currentDirty = isDraft ? draftState?.dirty ?? false : mcpDirty;
|
||||
const { mcpServerUrl } = getBackendConfig();
|
||||
const kandevTools = 'Tools: list_workspaces, list_boards, list_columns, list_tasks, create_task, update_task';
|
||||
|
||||
const popularServers: Record<string, Record<string, unknown>> = {
|
||||
kandev: {
|
||||
type: 'sse',
|
||||
url: 'http://localhost:9090/sse',
|
||||
url: mcpServerUrl,
|
||||
},
|
||||
playwright: {
|
||||
type: 'stdio',
|
||||
@@ -202,13 +206,20 @@ export function ProfileMcpConfigCard({
|
||||
MCP definitions are stored in the database and resolved per executor at runtime. This does not override your local agent config.</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">Popular servers</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs rounded-full border border-muted-foreground/30 px-2 py-1 hover:bg-muted cursor-pointer"
|
||||
onClick={() => applyPopularServer('kandev')}
|
||||
>
|
||||
+ Kandev MCP
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs rounded-full border border-muted-foreground/30 px-2 py-1 hover:bg-muted cursor-pointer"
|
||||
onClick={() => applyPopularServer('kandev')}
|
||||
>
|
||||
+ Kandev MCP
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-[320px] text-xs">
|
||||
{kandevTools}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs rounded-full border border-muted-foreground/30 px-2 py-1 hover:bg-muted cursor-pointer"
|
||||
|
||||
Vendored
+1
@@ -3,5 +3,6 @@ export {};
|
||||
declare global {
|
||||
interface Window {
|
||||
__KANDEV_API_BASE_URL?: string;
|
||||
__KANDEV_MCP_SERVER_URL?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
export type AppConfig = {
|
||||
apiBaseUrl: string;
|
||||
mcpServerUrl: string;
|
||||
};
|
||||
|
||||
const DEFAULT_API_BASE_URL = 'http://localhost:8080';
|
||||
const DEFAULT_MCP_SERVER_URL = 'http://localhost:9090/sse';
|
||||
export const DEBUG_UI = process.env.NEXT_PUBLIC_KANDEV_DEBUG === 'true';
|
||||
|
||||
export function getBackendConfig(): AppConfig {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
apiBaseUrl: process.env.KANDEV_API_BASE_URL ?? DEFAULT_API_BASE_URL,
|
||||
mcpServerUrl: process.env.KANDEV_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL,
|
||||
};
|
||||
}
|
||||
if (window.__KANDEV_API_BASE_URL) {
|
||||
if (window.__KANDEV_API_BASE_URL || window.__KANDEV_MCP_SERVER_URL) {
|
||||
return {
|
||||
apiBaseUrl: window.__KANDEV_API_BASE_URL,
|
||||
apiBaseUrl: window.__KANDEV_API_BASE_URL ?? DEFAULT_API_BASE_URL,
|
||||
mcpServerUrl: window.__KANDEV_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL,
|
||||
};
|
||||
}
|
||||
return {
|
||||
apiBaseUrl: process.env.NEXT_PUBLIC_KANDEV_API_BASE_URL ?? DEFAULT_API_BASE_URL,
|
||||
mcpServerUrl: process.env.NEXT_PUBLIC_KANDEV_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user