Files
cairnobs/hack/mock-ollama/main.go
T
jcoffey-dev 7a86008062 Complete the low-risk half of the Sentry -> Cairn OBS rebrand
Sweeps the references that carry no runtime coupling, and fixes one that
turned out to be a real bug rather than stale branding.

Docker network: sentry_default -> cairnobs_default across 23 runbook and
test-header `docker run` commands. Compose derives the network from the
directory name, so this lands together with renaming the working copy to
cairnobs/ -- the two are only correct as one change.

Stale references corrected: four Dockerfile "repo root (sentry/)"
headers; .env pointing at the long-renamed deploy/helm/sentry/ chart;
five Helm comments describing the topic as sentry.logs.raw when all four
code paths have defaulted to cairnobs.logs.raw for some time; an
absolute /home/john/Projects/sentry/ path in the operator's package doc,
now repo-relative; the hand-written Tenant CRD description in both of
its identical copies, whose Go source already said Cairn OBS.

Migration 0043 repoints the default tenant's data source. 0026 seeded it
with ('sentry', '/var/lib/sentry-search') to match what
api/internal/config then defaulted to; the rebrand later moved those
defaults to "cairnobs" and /var/lib/cairnobs-search without moving the
already-applied row, leaving the default tenant naming a ClickHouse
database nothing writes to. Scoped to the exact stale values so it is a
no-op on any deployment that set them deliberately. 0026's comment is
annotated as superseded; its applied SQL is untouched.

Deliberately not included: the gRPC wire packages (sentry.logs.v1,
sentry.agent.v1) and proto/sentry/ import paths, which cannot change
without a lockstep agent/server upgrade; the Helm chart's
sentry_metadata database and sentry role, which need a real Postgres
migration on existing deployments; and the compliance audit records in
docs/compliance/, which are a dated historical record.

go build, go vet, and go test pass for ingest and deploy/operator.
2026-08-22 18:39:25 -07:00

101 lines
3.3 KiB
Go

// Command mock-ollama stands in for a real Ollama server (see
// /docs/phase-7-ai-design.md) when manually verifying Phase 7's AI
// features against a live docker-compose stack without needing model
// weights or a GPU. Matches Ollama's real POST /api/chat wire contract
// (see api/ai/provider/ollama/ollama.go's chatRequest/chatResponse)
// closely enough that api/cmd/api and enterprise/cmd/enterprise-api
// can't tell the difference -- picks a canned, deterministic response by
// inspecting the system prompt's distinctive opening line (see
// api/ai/provider/ollama/prompts.go), the same technique the
// integration tests in api/ai/aiapi/integration_test.go use for the
// same reason: no live model, deterministic output, fast.
//
// Not part of any docker-compose service by default -- run it
// standalone (or as a throwaway container on the cairnobs_default
// network) and point OLLAMA_BASE_URL at it. See
// /docs/phase-7-runbook.md for the exact recipe.
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"strings"
)
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
}
type chatResponse struct {
Message chatMessage `json:"message"`
}
// canned responses, keyed by a distinctive substring of each
// operation's system prompt (see prompts.go's opening sentence for
// each). Content is deliberately valid, uninteresting pipe syntax --
// this tool exists to verify plumbing, not to simulate model quality.
var canned = []struct {
systemPromptContains string
response string
}{
{"translate a plain-English question", `{"query":"earliest=-1h severity=ERROR","confidence":"high"}`},
{"suggest how to continue", `{"suggestion":" severity=ERROR"}`},
{"phrase those findings", "Add a time range (e.g. earliest=-1h) to avoid scanning the entire table."},
{"fix a broken", `{"suggested_query":"earliest=-1h severity=ERROR","explanation":"added a missing time bound","confidence":"high"}`},
// Plain explain (no findings, no original-intent framing) falls
// through to this last, broadest match.
{"", "This query filters logs where severity equals ERROR from the last hour."},
}
func respond(system string) string {
for _, c := range canned {
if strings.Contains(system, c.systemPromptContains) {
return c.response
}
}
return canned[len(canned)-1].response
}
func main() {
addr := flag.String("addr", ":11434", "listen address")
flag.Parse()
http.HandleFunc("/api/chat", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "reading body: "+err.Error(), http.StatusBadRequest)
return
}
var req chatRequest
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "decoding request: "+err.Error(), http.StatusBadRequest)
return
}
var system string
for _, m := range req.Messages {
if m.Role == "system" {
system = m.Content
break
}
}
content := respond(system)
fmt.Printf("chat: model=%s -> %s\n", req.Model, content)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(chatResponse{Message: chatMessage{Role: "assistant", Content: content}})
})
log.Printf("mock-ollama listening on %s", *addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}