summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-09 10:00:03 -0500
committeradamdottv <[email protected]>2025-07-09 10:00:03 -0500
commit53f8e7850e235f484784d787a216b88c573dd5cd (patch)
treee3b23b6ad69fa092ac72db49eec3ec2654731bfd
parentca8ce88354fa2f8e7cc8f0a911197a2550ebc269 (diff)
downloadopencode-53f8e7850e235f484784d787a216b88c573dd5cd.tar.gz
opencode-53f8e7850e235f484784d787a216b88c573dd5cd.zip
feat: configurable log levels
-rw-r--r--packages/opencode/config.schema.json10
-rw-r--r--packages/opencode/src/config/config.ts1
-rw-r--r--packages/opencode/src/index.ts18
-rw-r--r--packages/opencode/src/server/server.ts5
-rw-r--r--packages/opencode/src/util/log.ts45
-rw-r--r--packages/tui/internal/util/apilogger.go14
-rw-r--r--packages/tui/sdk/.stats.yml6
-rw-r--r--packages/tui/sdk/api.md1
-rw-r--r--packages/tui/sdk/app.go21
-rw-r--r--packages/tui/sdk/app_test.go2
-rw-r--r--packages/tui/sdk/config.go3
-rw-r--r--packages/web/src/content/docs/docs/config.mdx22
-rw-r--r--stainless.yml1
13 files changed, 129 insertions, 20 deletions
diff --git a/packages/opencode/config.schema.json b/packages/opencode/config.schema.json
index b9d4a5ac9..18efb22c1 100644
--- a/packages/opencode/config.schema.json
+++ b/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": {
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 712ca3ff0..7c248da83 100644
--- a/packages/opencode/src/config/config.ts
+++ b/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({
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index f0328fe39..6206f230c 100644
--- a/packages/opencode/src/index.ts
+++ b/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),
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 23950cdba..38a808974 100644
--- a/packages/opencode/src/server/server.ts
+++ b/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
diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts
index f3928e13e..50749dae9 100644
--- a/packages/opencode/src/util/log.ts
+++ b/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
diff --git a/packages/tui/internal/util/apilogger.go b/packages/tui/internal/util/apilogger.go
index 11eb21722..b439bbecf 100644
--- a/packages/tui/internal/util/apilogger.go
+++ b/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 {
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index d0927c6a6..e3d985793 100644
--- a/packages/tui/sdk/.stats.yml
+++ b/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
diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md
index 5a9077ad8..c0304614a 100644
--- a/packages/tui/sdk/api.md
+++ b/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:
diff --git a/packages/tui/sdk/app.go b/packages/tui/sdk/app.go
index 39ca3bd9e..ba9303c80 100644
--- a/packages/tui/sdk/app.go
+++ b/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
diff --git a/packages/tui/sdk/app_test.go b/packages/tui/sdk/app_test.go
index 5e7f58c71..15f531ad4 100644
--- a/packages/tui/sdk/app_test.go
+++ b/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{}{
diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go
index 2f415c3fa..d6dc65f25 100644
--- a/packages/tui/sdk/config.go
+++ b/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
diff --git a/packages/web/src/content/docs/docs/config.mdx b/packages/web/src/content/docs/docs/config.mdx
index 40583ea0a..78552d415 100644
--- a/packages/web/src/content/docs/docs/config.mdx
+++ b/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.
diff --git a/stainless.yml b/stainless.yml
index 91f8043fc..9cc29c066 100644
--- a/stainless.yml
+++ b/stainless.yml
@@ -48,6 +48,7 @@ resources:
app:
models:
app: App
+ logLevel: LogLevel
methods:
get: get /app
init: post /app/init