Compare commits

..

5 Commits

Author SHA1 Message Date
Yuan Teoh
6048756e17 Merge branch 'main' into httplog 2026-02-13 11:07:19 -08:00
Yuan Teoh
d55666504d make a local copy of the schema instead of modifying it directly 2026-02-10 16:36:48 -08:00
Yuan Teoh
df0229c882 Update internal/log/log.go
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-10 16:29:01 -08:00
Yuan Teoh
d4959093c8 fix panic 2026-02-10 16:07:44 -08:00
Yuan Teoh
8ffafd7937 chore(deps): update httplog to v3 2026-02-10 10:56:47 -08:00
11 changed files with 124 additions and 113 deletions

View File

@@ -15,9 +15,6 @@ name: Link Checker
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
@@ -26,33 +23,8 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Identify Changed Files
id: changed-files
shell: bash
run: |
git fetch origin main
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/main...HEAD)
if [ -z "$CHANGED_FILES" ]; then
echo "No markdown files changed. Skipping checks."
echo "HAS_CHANGES=false" >> $GITHUB_ENV
else
echo "--- Changed Files to Scan ---"
echo "$CHANGED_FILES"
echo "-----------------------------"
# Flatten newlines to spaces for the args list
FILES_FLAT=$(echo "$CHANGED_FILES" | tr '\n' ' ')
echo "CHECK_FILES=$FILES_FLAT" >> $GITHUB_ENV
echo "HAS_CHANGES=true" >> $GITHUB_ENV
fi
- name: Restore lychee cache
if: env.HAS_CHANGES == 'true'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .lycheecache
@@ -61,7 +33,6 @@ jobs:
- name: Link Checker
id: lychee-check
if: env.HAS_CHANGES == 'true'
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2
continue-on-error: true
with:
@@ -71,7 +42,8 @@ jobs:
--cache
--max-cache-age 1d
--exclude '^neo4j\+.*' --exclude '^bolt://.*'
${{ env.CHECK_FILES }}
README.md
docs/
output: lychee-report.md
format: markdown
fail: true
@@ -79,44 +51,18 @@ jobs:
debug: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find comment
if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure'
uses: peter-evans/find-comment@v3
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: "## Link Resolution Note"
- name: Prepare Report
if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure'
run: |
# Create a new file 'full-report.md'
echo "## Link Resolution Note" > full-report.md
echo "Local links and directory changes work differently on GitHub than on the docsite." >> full-report.md
echo "You must ensure fixes pass the **GitHub check** and also work with **\`hugo server\`**." >> full-report.md
echo "See [Link Checking and Fixing with Lychee](https://github.com/googleapis/genai-toolbox/blob/main/DEVELOPER.md#link-checking-and-fixing-with-lychee) for more details." >> full-report.md
echo "---" >> full-report.md
echo "### Broken Links Found" >> full-report.md
# Clean the report (remove redirects) AND append it to the file
sed -E '/(Redirect|Redirects per input)/d' lychee-report.md >> full-report.md
- name: Create PR Comment
if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: full-report.md
edit-mode: replace
- name: Display Failure Report
# Run this ONLY if the link checker failed
if: steps.lychee-check.outcome == 'failure'
run: |
# We can now simply output the prepared file to the job summary
cat full-report.md >> $GITHUB_STEP_SUMMARY
# Fail the job
echo "## Link Resolution Note" >> $GITHUB_STEP_SUMMARY
echo "Local links and directory changes work differently on GitHub than on the docsite." >> $GITHUB_STEP_SUMMARY
echo "You must ensure fixes pass the **GitHub check** and also work with **\`hugo server\`**." >> $GITHUB_STEP_SUMMARY
echo "See [Link Checking and Fixing with Lychee](https://github.com/googleapis/genai-toolbox/blob/main/DEVELOPER.md#link-checking-and-fixing-with-lychee) for more details." >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "### Broken Links Found" >> $GITHUB_STEP_SUMMARY
cat ./lychee-report.md >> $GITHUB_STEP_SUMMARY
exit 1

View File

@@ -20,8 +20,6 @@ import (
"testing"
)
var goldenKeywords = []string{"Hilton Basel", "Hyatt Regency", "book"}
func TestQuickstartSample(t *testing.T) {
framework := os.Getenv("ORCH_NAME")
if framework == "" {
@@ -61,10 +59,16 @@ func TestQuickstartSample(t *testing.T) {
t.Fatal("Script ran successfully but produced no output.")
}
goldenFile, err := os.ReadFile("../golden.txt")
if err != nil {
t.Fatalf("Could not read golden.txt to check for keywords: %v", err)
}
keywords := strings.Split(string(goldenFile), "\n")
var missingKeywords []string
outputLower := strings.ToLower(actualOutput)
for _, keyword := range goldenKeywords {
for _, keyword := range keywords {
kw := strings.TrimSpace(keyword)
if kw != "" && !strings.Contains(outputLower, strings.ToLower(kw)) {
missingKeywords = append(missingKeywords, kw)

View File

@@ -0,0 +1,3 @@
Hilton Basel
Hyatt Regency
book

View File

@@ -25,7 +25,7 @@ const quickstartPath = path.join(orchDir, "quickstart.js");
const { main: runAgent } = await import(quickstartPath);
const GOLDEN_KEYWORDS = ["Hilton Basel", "Hyatt Regency", "book"];
const GOLDEN_FILE_PATH = path.resolve(__dirname, "../golden.txt");
describe(`${ORCH_NAME} Quickstart Agent`, () => {
let capturedOutput = [];
@@ -52,8 +52,11 @@ describe(`${ORCH_NAME} Quickstart Agent`, () => {
"Assertion Failed: Script ran successfully but produced no output."
);
const goldenFile = fs.readFileSync(GOLDEN_FILE_PATH, "utf8");
const keywords = goldenFile.split("\n").filter((kw) => kw.trim() !== "");
const missingKeywords = [];
for (const keyword of GOLDEN_KEYWORDS) {
for (const keyword of keywords) {
if (!actualOutput.toLowerCase().includes(keyword.toLowerCase())) {
missingKeywords.push(keyword);
}

View File

@@ -24,7 +24,18 @@ module_path = f"python.{ORCH_NAME}.quickstart"
quickstart = importlib.import_module(module_path)
GOLDEN_KEYWORDS = ["Hilton Basel", "Hyatt Regency", "book"]
@pytest.fixture(scope="module")
def golden_keywords():
"""Loads expected keywords from the golden.txt file."""
golden_file_path = Path("../golden.txt")
if not golden_file_path.exists():
pytest.fail(f"Golden file not found: {golden_file_path}")
try:
with open(golden_file_path, 'r') as f:
return [line.strip() for line in f.readlines() if line.strip()]
except Exception as e:
pytest.fail(f"Could not read golden.txt: {e}")
# --- Execution Tests ---
class TestExecution:
@@ -51,8 +62,8 @@ class TestExecution:
"""Test that the script runs and produces no stderr."""
assert script_output.err == "", f"Script produced stderr: {script_output.err}"
def test_keywords_in_output(self, script_output):
def test_keywords_in_output(self, script_output, golden_keywords):
"""Test that expected keywords are present in the script's output."""
output = script_output.out
missing_keywords = [kw for kw in GOLDEN_KEYWORDS if kw.lower() not in output.lower()]
missing_keywords = [kw for kw in golden_keywords if kw not in output]
assert not missing_keywords, f"Missing keywords in output: {missing_keywords}"

View File

@@ -207,6 +207,7 @@ You can connect to Toolbox Cloud Run instances directly through the SDK.
{{< tab header="Python" lang="python" >}}
import asyncio
from toolbox_core import ToolboxClient, auth_methods
from toolbox_core.protocol import Protocol
# Replace with the Cloud Run service URL generated in the previous step
URL = "https://cloud-run-url.app"
@@ -217,6 +218,7 @@ async def main():
async with ToolboxClient(
URL,
client_headers={"Authorization": auth_token_provider},
protocol=Protocol.TOOLBOX,
) as toolbox:
toolset = await toolbox.load_toolset()
# ...

2
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/httplog/v2 v2.1.1
github.com/go-chi/httplog/v3 v3.3.0
github.com/go-chi/render v1.0.3
github.com/go-goquery/goquery v1.0.1
github.com/go-playground/validator/v10 v10.28.0

4
go.sum
View File

@@ -902,8 +902,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk=
github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE=
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=

View File

@@ -59,25 +59,35 @@ func NewStdLogger(outW, errW io.Writer, logLevel string) (Logger, error) {
}
// DebugContext logs debug messages
func (sl *StdLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StdLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.outLogger.DebugContext(ctx, msg, keysAndValues...)
}
// InfoContext logs debug messages
func (sl *StdLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StdLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.outLogger.InfoContext(ctx, msg, keysAndValues...)
}
// WarnContext logs warning messages
func (sl *StdLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StdLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.errLogger.WarnContext(ctx, msg, keysAndValues...)
}
// ErrorContext logs error messages
func (sl *StdLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StdLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.errLogger.ErrorContext(ctx, msg, keysAndValues...)
}
// SlogLogger returns a single standard *slog.Logger that routes
// records to the outLogger or errLogger based on the log level.
func (sl *StdLogger) SlogLogger() *slog.Logger {
splitHandler := &SplitHandler{
OutHandler: sl.outLogger.Handler(),
ErrHandler: sl.errLogger.Handler(),
}
return slog.New(splitHandler)
}
const (
Debug = "DEBUG"
Info = "INFO"
@@ -177,21 +187,64 @@ func NewStructuredLogger(outW, errW io.Writer, logLevel string) (Logger, error)
}
// DebugContext logs debug messages
func (sl *StructuredLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StructuredLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.outLogger.DebugContext(ctx, msg, keysAndValues...)
}
// InfoContext logs info messages
func (sl *StructuredLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StructuredLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.outLogger.InfoContext(ctx, msg, keysAndValues...)
}
// WarnContext logs warning messages
func (sl *StructuredLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StructuredLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.errLogger.WarnContext(ctx, msg, keysAndValues...)
}
// ErrorContext logs error messages
func (sl *StructuredLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
func (sl *StructuredLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...any) {
sl.errLogger.ErrorContext(ctx, msg, keysAndValues...)
}
// SlogLogger returns a single standard *slog.Logger that routes
// records to the outLogger or errLogger based on the log level.
func (sl *StructuredLogger) SlogLogger() *slog.Logger {
splitHandler := &SplitHandler{
OutHandler: sl.outLogger.Handler(),
ErrHandler: sl.errLogger.Handler(),
}
return slog.New(splitHandler)
}
type SplitHandler struct {
OutHandler slog.Handler
ErrHandler slog.Handler
}
func (h *SplitHandler) Enabled(ctx context.Context, level slog.Level) bool {
if level >= slog.LevelWarn {
return h.ErrHandler.Enabled(ctx, level)
}
return h.OutHandler.Enabled(ctx, level)
}
func (h *SplitHandler) Handle(ctx context.Context, r slog.Record) error {
if r.Level >= slog.LevelWarn {
return h.ErrHandler.Handle(ctx, r)
}
return h.OutHandler.Handle(ctx, r)
}
func (h *SplitHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &SplitHandler{
OutHandler: h.OutHandler.WithAttrs(attrs),
ErrHandler: h.ErrHandler.WithAttrs(attrs),
}
}
func (h *SplitHandler) WithGroup(name string) slog.Handler {
return &SplitHandler{
OutHandler: h.OutHandler.WithGroup(name),
ErrHandler: h.ErrHandler.WithGroup(name),
}
}

View File

@@ -16,16 +16,20 @@ package log
import (
"context"
"log/slog"
)
// Logger is the interface used throughout the project for logging.
type Logger interface {
// DebugContext is for reporting additional information about internal operations.
DebugContext(ctx context.Context, format string, args ...interface{})
DebugContext(ctx context.Context, format string, args ...any)
// InfoContext is for reporting informational messages.
InfoContext(ctx context.Context, format string, args ...interface{})
InfoContext(ctx context.Context, format string, args ...any)
// WarnContext is for reporting warning messages.
WarnContext(ctx context.Context, format string, args ...interface{})
WarnContext(ctx context.Context, format string, args ...any)
// ErrorContext is for reporting errors.
ErrorContext(ctx context.Context, format string, args ...interface{})
ErrorContext(ctx context.Context, format string, args ...any)
// Single standard slog.Logger that routes records to the outLogger or
// errLogger based on log levels
SlogLogger() *slog.Logger
}

View File

@@ -28,7 +28,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/httplog/v2"
"github.com/go-chi/httplog/v3"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/log"
@@ -347,31 +347,16 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
if err != nil {
return nil, fmt.Errorf("unable to initialize http log: %w", err)
}
var httpOpts httplog.Options
switch cfg.LoggingFormat.String() {
case "json":
httpOpts = httplog.Options{
JSON: true,
LogLevel: logLevel,
Concise: true,
RequestHeaders: false,
MessageFieldName: "message",
SourceFieldName: "logging.googleapis.com/sourceLocation",
TimeFieldName: "timestamp",
LevelFieldName: "severity",
}
case "standard":
httpOpts = httplog.Options{
LogLevel: logLevel,
Concise: true,
RequestHeaders: false,
MessageFieldName: "message",
}
default:
return nil, fmt.Errorf("invalid Logging format: %q", cfg.LoggingFormat.String())
schema := *httplog.SchemaGCP
schema.Level = cfg.LogLevel.String()
schema.Concise(true)
httpOpts := &httplog.Options{
Level: logLevel,
Schema: &schema,
}
httpLogger := httplog.NewLogger("httplog", httpOpts)
r.Use(httplog.RequestLogger(httpLogger))
logger := l.SlogLogger()
r.Use(httplog.RequestLogger(logger, httpOpts))
sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := InitializeConfigs(ctx, cfg)
if err != nil {