Documentation
¶
Overview ¶
Package studio is the Inspector: the UI, the JSON API, the live stream and the OTLP receiver over one observability database (weft/obsdb), served by Go alone.
The three setups (S4.6) ¶
Setup A embeds the handler next to the app's own pipeline — the five lines of WEFT-OTEL-DATA-ARCHITECTURE §10.1:
defer otel.Install()() // local sink ./.weft/weft.db, content on, no network
mux.Handle("/studio/", http.StripPrefix("/studio",
studio.Handler(studio.DB(otel.LocalDB())))) // same DB, same live hub [D4]
Passing the pipeline's handle is what makes it live: writes publish to the handle's hub and the /api/live stream follows, no network (examples/studio-local is the whole thing, with a thread session). Without a Token the API answers only a loopback Host (localhost, *.localhost, 127.0.0.0/8, [::1]) or one an AllowOrigins origin names — DNS rebinding makes any site same-origin to a loopback Studio — so an app served on a real hostname lists its origin in AllowOrigins or sets a Token.
Setup B runs the binary (studio/cmd): the UI, OTLP ingest, SQLite and a dev token on 127.0.0.1:7331 — any language's app points WEFT_STUDIO_URL or OTEL_EXPORTER_OTLP_ENDPOINT at it.
Setup C hosts it behind a Token: the panel's scoped tokens are HMAC-signed public-id handles minted through POST /api/panel-tokens.
Routes are groups; capabilities are computed ¶
The serving surface is a list of route groups (routes.go): New registers the core groups (the read API, the live stream, ingest, panel-token minting), and the lanes that follow add theirs in their own files without editing anyone else's:
- step 7 (lane C1) adds studio/panel.go, which sets panelGroupHook through a package-level var initializer — no init(), no registry — and the panel group is always on;
- step 8 (lane C2) adds studio/playground.go the same way (playgroundGroupHook), enabled by the Playground(true) option.
Both files exist now: the panel group registers always and names no capability (the panel is a client of the API), and Playground(true) registers the playground's groups — capabilities playground, runtimes, breakpoints and steer. api/meta's capabilities list is computed from the registered groups (live, ingest, auth with a Token, and the playground's) — never hard-coded — plus anything a hosting wrapper declares with Capabilities(...). The UI gates every deployment-specific screen on those names (ADR 0018 §8).
The API is the contract ¶
The JSON API (S4.2/S4.3) is the one contract between Go and the UI (and the panel): its types are written by hand in api.go and mirrored in web/src/lib/api.ts, pinned on the Go side by golden tests (testdata/api) and on the TS side by type-checking. Events are paged (ADR 0018 §8); the live tail is GET /api/live, an SSE stream whose frame ids are the hub's Seq — the resume cursor.
Index ¶
- Constants
- func DevToken() string
- func Handler(opts ...Option) http.Handler
- type Option
- func AllowOrigins(origins ...string) Option
- func Base(path string) Option
- func Capabilities(names ...string) Option
- func DB(db obsdb.DB) Option
- func IngestToken(tok string) Option
- func Live(h obsdb.Hub) Option
- func Manifest(json []byte) Option
- func NoIngest() Option
- func Open(path string) Option
- func Playground(on bool) Option
- func Title(s string) Option
- func Token(tok string) Option
- type RuntimeServer
- type Server
Examples ¶
Constants ¶
const Version = "v0.4.1"
Version is the studio module's tag, reported by api/meta. It moves when the module is released, nothing else.
Variables ¶
This section is empty.
Functions ¶
func DevToken ¶
func DevToken() string
DevToken generates a random dev token for setup B's binary: printed at start, fixed by WEFT_STUDIO_TOKEN. Exported because the binary lives in its own module.
func Handler ¶
Handler is New(opts...).Handler(): the one-liner for setups A and B when the playground is not in play.
Example ¶
ExampleHandler is setup A's shape on one screen: a Studio over an obsdb handle, mounted under a prefix. otel.Install's local sink writes the handle in a real app; here a batch stands in for it.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"github.com/weftgo/weft/studio"
"github.com/weftgo/weft/obsdb/sqlite"
)
func main() {
dir, err := os.MkdirTemp("", "weft-studio-example-")
if err != nil {
fmt.Println(err)
return
}
defer func() { _ = os.RemoveAll(dir) }()
db, err := sqlite.Open(dir + "/weft.db")
if err != nil {
fmt.Println(err)
return
}
defer func() { _ = db.Close() }()
// The mount the doc comment shows; the server serves the UI, the
// API, the live stream and OTLP ingest under /studio/.
mux := http.NewServeMux()
mux.Handle("/studio/", http.StripPrefix("/studio",
studio.Handler(studio.DB(db))))
srv := httptest.NewServer(mux)
defer srv.Close()
resp, err := http.Get(srv.URL + "/studio/api/meta")
if err != nil {
fmt.Println(err)
return
}
defer func() { _ = resp.Body.Close() }()
fmt.Println(resp.StatusCode)
}
Output: 200
Types ¶
type Option ¶
type Option func(*config)
Option configures New.
func AllowOrigins ¶
AllowOrigins permits these origins on the API and ingest routes (CORS for a panel served from another origin). The default, when a Token is configured, is localhost and 127.0.0.1 on any port; setup A (same origin, no token) sends no CORS headers unless origins are listed. Without a Token each origin's host:port is also a Host the API answers besides the loopback names (an app served at http://myapp.internal:8080 lists exactly that); "*" admits every Host.
func Base ¶
Base is the URL path Studio is mounted at, with leading and trailing slash. Default "/studio/". It is written into the shell's <base href> per request and becomes the router's basepath, so the same bundle mounts anywhere (ADR 0018 §6).
func Capabilities ¶
Capabilities declares named API capabilities the backing server provides beyond what the registered route groups already report (ADR 0018 §8). The core groups contribute their own names — live, ingest, auth — computed from what is actually registered, never hard-coded; this option is for a hosting wrapper's own verbs.
func DB ¶
DB serves Studio over db (setup A: DB(otel.LocalDB()) — the same handle weft/otel's Local destination writes, so the UI reads what the run wrote, live, through the handle's own hub). Without DB or Open, New opens the history database at $WEFT_DB or ./.weft/weft.db (history only: a second handle on the file, no live lane — passing the pipeline's handle is what makes setup A live). A nil db panics at New time — a Studio with nothing to read is a construction error.
func IngestToken ¶
IngestToken requires `Authorization: Bearer <tok>` on the OTLP ingest routes. Without one, ingest answers loopback peers only (setup B's dev mode) — a Studio bound wide without a token refuses remote exporters (S4.4).
func Live ¶
Live serves the live stream from h (S4.1). The default is the DB's own hub when it implements Hub() — setup A's sub-100 ms lane — else an in-process obsdb.NewHub() fed by ingest.
func Manifest ¶
Manifest supplies core.Manifest bytes for the agent and tool cards. Without it api/manifest answers 404 and the UI hides the Agents nav.
func NoIngest ¶
func NoIngest() Option
NoIngest turns the OTLP ingest routes off: a read-only Studio. The ingest capability disappears from api/meta with them.
func Open ¶
Open is shorthand for DB(sqlite.Open(path)): Studio over an obsdb sqlite file, created when missing. Opening happens at New time and panics on failure — a Studio that cannot reach its database is a construction error, not a serving one.
func Playground ¶
Playground turns the runtime link server and the playground routes on (step 8: studio/playground.go registers its group, which this option enables). Until that file exists the option is accepted and does nothing, and meta.capabilities does not list the playground.
func Token ¶
Token protects the JSON API and the panel-token mint with a bearer token, and keys the panel tokens' HMAC signatures (setup C's signing key). Without it the API is open to a loopback Host or one AllowOrigins names (setup A: embedded, same origin; any other Host is refused, the DNS-rebinding guard).
type RuntimeServer ¶
type RuntimeServer = linkrt.RuntimeServer
RuntimeServer is the runtime link server's in-process side (studio/runtime): the registry of connected runtimes that weft/runtime's runtime.Local(srv) drives and Server.Runtime returns — an alias, so the S4.1 name stays in this package while the type lives where the routes do. Nil on a Server built without Playground(true); the runtime-link routes register with the playground group.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is one Studio: the embedded UI, the JSON API, the live stream and — unless disabled — OTLP ingest, over one obsdb.DB. Build it with New; serve it with Handler.
Example ¶
ExampleServer shows the Server surface (S4.1): New when the playground is in play, Handler() to serve, Close for what New opened, Runtime() nil without Playground(true). New without DB or Open would open the default path ($WEFT_DB or ./.weft/weft.db) — pinned by TestOpenOption — so the example opens a throwaway sqlite file instead of writing into the package directory.
package main
import (
"fmt"
"os"
"github.com/weftgo/weft/studio"
)
func main() {
dir, err := os.MkdirTemp("", "weft-studio-example-")
if err != nil {
fmt.Println(err)
return
}
defer func() { _ = os.RemoveAll(dir) }()
srv := studio.New(studio.Open(dir+"/weft.db"), studio.NoIngest()) // read-only: no OTLP receiver
defer func() { _ = srv.Close() }() // New opened that DB: closed here
_ = srv.Handler() // mount it; the binary in studio/cmd does
_ = srv.Runtime() // nil without Playground(true) (studio/runtime)
fmt.Println("ok")
}
Output: ok
func New ¶
New builds a Studio server over one obsdb.DB (S4.1). Handler() serves the UI, the panel, the JSON API, the live stream and — unless disabled — OTLP ingest. Runtime() is the runtime link server's in-process side, which weft/runtime takes in setup A; nil unless Playground(true) built it.
func (*Server) Close ¶
Close releases what the server owns: the database when New opened it (Open or the default path). A DB passed through DB(...) stays its owner's — closing otel's handle out from under the pipeline is not Studio's call.
func (*Server) Handler ¶
Handler serves Studio. Mount it under a prefix with http.StripPrefix, or at the root of its own mux:
mux.Handle("/studio/", http.StripPrefix("/studio",
studio.Handler(studio.DB(db))))
The handler does not bind a port — the caller chooses the address. Bind loopback, or configure Token, before exposing it wider.
func (*Server) Runtime ¶
func (s *Server) Runtime() *RuntimeServer
Runtime returns the runtime link server's in-process side (what weft/runtime's runtime.Local takes): the server Playground(true) built, nil without the option — and without it the playground routes do not exist either.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Command studio is setup B's local binary (S4.6, §10.1): the UI, OTLP ingest, SQLite and a dev token on 127.0.0.1:7331 — the zero-infra Studio for any language's app:
|
Command studio is setup B's local binary (S4.6, §10.1): the UI, OTLP ingest, SQLite and a dev token on 127.0.0.1:7331 — the zero-infra Studio for any language's app: |
|
examples
|
|
|
basic
command
Command basic records demo runs — a tool call, a subagent, a failure — into an obsdb sqlite database and serves Studio on 127.0.0.1:7331:
|
Command basic records demo runs — a tool call, a subagent, a failure — into an obsdb sqlite database and serves Studio on 127.0.0.1:7331: |
|
Package ingest is Studio's OTLP/HTTP receiver (S4.4): POST /v1/traces and /v1/logs, protobuf and JSON, optional gzip, a 16 MiB limit on the decompressed body, and the publish-then-write pipeline —
|
Package ingest is Studio's OTLP/HTTP receiver (S4.4): POST /v1/traces and /v1/logs, protobuf and JSON, optional gzip, a 16 MiB limit on the decompressed body, and the publish-then-write pipeline — |
|
Package runtime is the runtime link's server side (WEFT-PLAYGROUND §10.3, S4.2): the registry of connected runtimes and the three routes a weft/runtime client speaks to — register, the SSE command stream, acks.
|
Package runtime is the runtime link's server side (WEFT-PLAYGROUND §10.3, S4.2): the registry of connected runtimes and the three routes a weft/runtime client speaks to — register, the SSE command stream, acks. |

