Browse Source

feat: configurable log levels

adamdottv 7 months ago
parent
commit
53f8e7850e

+ 10 - 0
packages/opencode/config.schema.json

@@ -299,6 +299,16 @@
       "type": "string",
       "description": "Model to use in the format of provider/model, eg anthropic/claude-2"
     },
+    "log_level": {
+      "type": "string",
+      "enum": [
+        "DEBUG",
+        "INFO",
+        "WARN",
+        "ERROR"
+      ],
+      "description": "Minimum log level to write to log files"
+    },
     "provider": {
       "type": "object",
       "additionalProperties": {

+ 1 - 0
packages/opencode/src/config/config.ts

@@ -108,6 +108,7 @@ export namespace Config {
       autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
       disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
       model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
+      log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
       provider: z
         .record(
           ModelsDev.Provider.partial().extend({

+ 18 - 0
packages/opencode/src/index.ts

@@ -40,6 +40,24 @@ const cli = yargs(hideBin(process.argv))
   })
   .middleware(async () => {
     await Log.init({ print: process.argv.includes("--print-logs") })
+
+    try {
+      const { Config } = await import("./config/config")
+      const { App } = await import("./app/app")
+
+      App.provide({ cwd: process.cwd() }, async () => {
+        const cfg = await Config.get()
+        if (cfg.log_level) {
+          Log.setLevel(cfg.log_level as Log.Level)
+        } else {
+          const defaultLevel = Installation.isDev() ? "DEBUG" : "INFO"
+          Log.setLevel(defaultLevel)
+        }
+      })
+    } catch (e) {
+      Log.Default.error("failed to load config", { error: e })
+    }
+
     Log.Default.info("opencode", {
       version: Installation.VERSION,
       args: process.argv.slice(2),

+ 4 - 1
packages/opencode/src/server/server.ts

@@ -651,7 +651,7 @@ export namespace Server {
           "json",
           z.object({
             service: z.string().openapi({ description: "Service name for the log entry" }),
-            level: z.enum(["info", "error", "warn"]).openapi({ description: "Log level" }),
+            level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }),
             message: z.string().openapi({ description: "Log message" }),
             extra: z
               .record(z.string(), z.any())
@@ -664,6 +664,9 @@ export namespace Server {
           const logger = Log.create({ service })
 
           switch (level) {
+            case "debug":
+              logger.debug(message, extra)
+              break
             case "info":
               logger.info(message, extra)
               break

+ 42 - 3
packages/opencode/src/util/log.ts

@@ -1,8 +1,35 @@
 import path from "path"
 import fs from "fs/promises"
 import { Global } from "../global"
+import z from "zod"
+
 export namespace Log {
+  export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).openapi({ ref: "LogLevel", description: "Log level" })
+  export type Level = z.infer<typeof Level>
+
+  const levelPriority: Record<Level, number> = {
+    DEBUG: 0,
+    INFO: 1,
+    WARN: 2,
+    ERROR: 3,
+  }
+
+  let currentLevel: Level = "INFO"
+
+  export function setLevel(level: Level) {
+    currentLevel = level
+  }
+
+  export function getLevel(): Level {
+    return currentLevel
+  }
+
+  function shouldLog(level: Level): boolean {
+    return levelPriority[level] >= levelPriority[currentLevel]
+  }
+
   export type Logger = {
+    debug(message?: any, extra?: Record<string, any>): void
     info(message?: any, extra?: Record<string, any>): void
     error(message?: any, extra?: Record<string, any>): void
     warn(message?: any, extra?: Record<string, any>): void
@@ -23,6 +50,7 @@ export namespace Log {
 
   export interface Options {
     print: boolean
+    level?: Level
   }
 
   let logpath = ""
@@ -85,14 +113,25 @@ export namespace Log {
       return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
     }
     const result: Logger = {
+      debug(message?: any, extra?: Record<string, any>) {
+        if (shouldLog("DEBUG")) {
+          process.stderr.write("DEBUG " + build(message, extra))
+        }
+      },
       info(message?: any, extra?: Record<string, any>) {
-        process.stderr.write("INFO  " + build(message, extra))
+        if (shouldLog("INFO")) {
+          process.stderr.write("INFO  " + build(message, extra))
+        }
       },
       error(message?: any, extra?: Record<string, any>) {
-        process.stderr.write("ERROR " + build(message, extra))
+        if (shouldLog("ERROR")) {
+          process.stderr.write("ERROR " + build(message, extra))
+        }
       },
       warn(message?: any, extra?: Record<string, any>) {
-        process.stderr.write("WARN  " + build(message, extra))
+        if (shouldLog("WARN")) {
+          process.stderr.write("WARN  " + build(message, extra))
+        }
       },
       tag(key: string, value: string) {
         if (tags) tags[key] = value

+ 3 - 11
packages/tui/internal/util/apilogger.go

@@ -8,7 +8,6 @@ import (
 	opencode "github.com/sst/opencode-sdk-go"
 )
 
-// APILogHandler is a slog.Handler that sends logs to the opencode API
 type APILogHandler struct {
 	client  *opencode.Client
 	service string
@@ -18,7 +17,6 @@ type APILogHandler struct {
 	mu      sync.Mutex
 }
 
-// NewAPILogHandler creates a new APILogHandler
 func NewAPILogHandler(client *opencode.Client, service string, level slog.Level) *APILogHandler {
 	return &APILogHandler{
 		client:  client,
@@ -29,17 +27,16 @@ func NewAPILogHandler(client *opencode.Client, service string, level slog.Level)
 	}
 }
 
-// Enabled reports whether the handler handles records at the given level.
 func (h *APILogHandler) Enabled(_ context.Context, level slog.Level) bool {
 	return level >= h.level
 }
 
-// Handle handles the Record.
 func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
-	// Convert slog level to API level
 	var apiLevel opencode.AppLogParamsLevel
 	switch r.Level {
-	case slog.LevelDebug, slog.LevelInfo:
+	case slog.LevelDebug:
+		apiLevel = opencode.AppLogParamsLevelDebug
+	case slog.LevelInfo:
 		apiLevel = opencode.AppLogParamsLevelInfo
 	case slog.LevelWarn:
 		apiLevel = opencode.AppLogParamsLevelWarn
@@ -49,23 +46,19 @@ func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
 		apiLevel = opencode.AppLogParamsLevelInfo
 	}
 
-	// Build extra fields
 	extra := make(map[string]any)
 
-	// Add handler attributes
 	h.mu.Lock()
 	for _, attr := range h.attrs {
 		extra[attr.Key] = attr.Value.Any()
 	}
 	h.mu.Unlock()
 
-	// Add record attributes
 	r.Attrs(func(attr slog.Attr) bool {
 		extra[attr.Key] = attr.Value.Any()
 		return true
 	})
 
-	// Send log to API
 	params := opencode.AppLogParams{
 		Service: opencode.F(h.service),
 		Level:   opencode.F(apiLevel),
@@ -76,7 +69,6 @@ func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
 		params.Extra = opencode.F(extra)
 	}
 
-	// Use a goroutine to avoid blocking the logger
 	go func() {
 		_, err := h.client.App.Log(context.Background(), params)
 		if err != nil {

+ 3 - 3
packages/tui/sdk/.stats.yml

@@ -1,4 +1,4 @@
 configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-3ae2247ea9674e156e5ad818e13d8cd8622737ee1b95fdcde23ebf50963df13c.yml
-openapi_spec_hash: 3075cca003eb61c035d3eb5891a6c38c
-config_hash: a2751b16a52007a1e12967ab4aa3729f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-879570c29c56e0a73a0624a84662b7f7c319a3c790c78ec6ac4cf62a7b1a5bd0.yml
+openapi_spec_hash: 2432e2dfed22193a0c6b3dfe0f82ec7d
+config_hash: 53e3aeb355f3b2e0d10985d6d7635a7e

+ 1 - 0
packages/tui/sdk/api.md

@@ -19,6 +19,7 @@ Methods:
 Response Types:
 
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#LogLevel">LogLevel</a>
 
 Methods:
 

+ 20 - 1
packages/tui/sdk/app.go

@@ -131,6 +131,24 @@ func (r appTimeJSON) RawJSON() string {
 	return r.raw
 }
 
+// Log level
+type LogLevel string
+
+const (
+	LogLevelDebug LogLevel = "DEBUG"
+	LogLevelInfo  LogLevel = "INFO"
+	LogLevelWarn  LogLevel = "WARN"
+	LogLevelError LogLevel = "ERROR"
+)
+
+func (r LogLevel) IsKnown() bool {
+	switch r {
+	case LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelError:
+		return true
+	}
+	return false
+}
+
 type AppLogParams struct {
 	// Log level
 	Level param.Field[AppLogParamsLevel] `json:"level,required"`
@@ -150,6 +168,7 @@ func (r AppLogParams) MarshalJSON() (data []byte, err error) {
 type AppLogParamsLevel string
 
 const (
+	AppLogParamsLevelDebug AppLogParamsLevel = "debug"
 	AppLogParamsLevelInfo  AppLogParamsLevel = "info"
 	AppLogParamsLevelError AppLogParamsLevel = "error"
 	AppLogParamsLevelWarn  AppLogParamsLevel = "warn"
@@ -157,7 +176,7 @@ const (
 
 func (r AppLogParamsLevel) IsKnown() bool {
 	switch r {
-	case AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
+	case AppLogParamsLevelDebug, AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
 		return true
 	}
 	return false

+ 1 - 1
packages/tui/sdk/app_test.go

@@ -70,7 +70,7 @@ func TestAppLogWithOptionalParams(t *testing.T) {
 		option.WithBaseURL(baseURL),
 	)
 	_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
-		Level:   opencode.F(opencode.AppLogParamsLevelInfo),
+		Level:   opencode.F(opencode.AppLogParamsLevelDebug),
 		Message: opencode.F("message"),
 		Service: opencode.F("service"),
 		Extra: opencode.F(map[string]interface{}{

+ 3 - 0
packages/tui/sdk/config.go

@@ -62,6 +62,8 @@ type Config struct {
 	Instructions []string `json:"instructions"`
 	// Custom keybind configurations
 	Keybinds Keybinds `json:"keybinds"`
+	// Minimum log level to write to log files
+	LogLevel LogLevel `json:"log_level"`
 	// MCP (Model Context Protocol) server configurations
 	Mcp map[string]ConfigMcp `json:"mcp"`
 	// Model to use in the format of provider/model, eg anthropic/claude-2
@@ -82,6 +84,7 @@ type configJSON struct {
 	Experimental      apijson.Field
 	Instructions      apijson.Field
 	Keybinds          apijson.Field
+	LogLevel          apijson.Field
 	Mcp               apijson.Field
 	Model             apijson.Field
 	Provider          apijson.Field

+ 22 - 0
packages/web/src/content/docs/docs/config.mdx

@@ -93,6 +93,28 @@ You can configure MCP servers you want to use through the `mcp` option.
 
 ---
 
+### Logging
+
+You can configure the minimum log level through the `log_level` option. This controls which log messages are written to the log files.
+
+```json title="opencode.json"
+{
+  "$schema": "https://opencode.ai/config.json",
+  "log_level": "INFO"
+}
+```
+
+Available log levels are:
+
+- `DEBUG` - All messages including debug information
+- `INFO` - Informational messages and above (default)
+- `WARN` - Warnings and errors only
+- `ERROR` - Errors only
+
+The default log level is `INFO` in production and `DEBUG` in development mode.
+
+---
+
 ### Disabled providers
 
 You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.

+ 1 - 0
stainless.yml

@@ -48,6 +48,7 @@ resources:
   app:
     models:
       app: App
+      logLevel: LogLevel
     methods:
       get: get /app
       init: post /app/init