Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/ayd/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Options:
Ayd won't create log file if set "-" or empty.
You can use time spec %Y, %y, %m, %d, %H, %M, in the file name.
(default "ayd_%Y%m%d.log")
-n, --name=NAME Instance name. This will be shown in page titles and logs.
-p, --port=PORT Listen port of status page. (default 9000)
-u, --user=USER[:PASS] Username and password for HTTP basic auth.
-c, --ssl-cert=FILE Path to certificate file for HTTPS. Please set also -k.
Expand Down
26 changes: 12 additions & 14 deletions cmd/ayd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,20 @@ import (
"github.com/spf13/pflag"
)

func init() {
scheme.HTTPUserAgent = fmt.Sprintf("ayd/%s health check", meta.Version)
}

type AydCommand struct {
OutStream io.Writer
ErrStream io.Writer

ListenPort int
StorePath string
OneshotMode bool
AlertURLs []string
UserInfo string
CertPath string
KeyPath string
ShowVersion bool
ShowHelp bool
ListenPort int
StorePath string
InstanceName string
OneshotMode bool
AlertURLs []string
UserInfo string
CertPath string
KeyPath string
ShowVersion bool
ShowHelp bool

Tasks []Task
StartedAt time.Time
Expand Down Expand Up @@ -60,6 +57,7 @@ func (cmd *AydCommand) ParseArgs(args []string) (exitCode int) {

flags.IntVarP(&cmd.ListenPort, "port", "p", 9000, "HTTP listen port")
flags.StringVarP(&cmd.StorePath, "log-file", "f", "ayd_%Y%m%d.log", "Path to log file")
flags.StringVarP(&cmd.InstanceName, "name", "n", "", "Instance name")
flags.BoolVarP(&cmd.OneshotMode, "oneshot", "1", false, "Check status only once and exit")
flags.StringArrayVarP(&cmd.AlertURLs, "alert", "a", nil, "The alert URLs")
flags.StringVarP(&cmd.UserInfo, "user", "u", "", "Username and password for HTTP endpoint")
Expand Down Expand Up @@ -133,7 +131,7 @@ func (cmd *AydCommand) Run(args []string) (exitCode int) {
return 0
}

s, err := store.New(cmd.StorePath, cmd.OutStream)
s, err := store.New(cmd.InstanceName, cmd.StorePath, cmd.OutStream)
if err != nil {
fmt.Fprintf(cmd.ErrStream, "error: failed to open log file: %s\n", err)
return 1
Expand Down
18 changes: 18 additions & 0 deletions cmd/ayd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,24 @@ func TestAydCommand_ParseArgs(t *testing.T) {
Pattern: "invalid argument:\n ::invalid URL: Not valid as schedule or target URL.\n\nPlease see `ayd -h` for more information\\.\n",
ExitCode: 2,
},
{
Args: []string{"ayd", "dummy:"},
ExitCode: 0,
Extra: func(t *testing.T, cmd main.AydCommand) {
if cmd.InstanceName != "" {
t.Errorf("expected InstanceName is empty in default but got %q", cmd.InstanceName)
}
},
},
{
Args: []string{"ayd", "-n", "Test Instance", "dummy:"},
ExitCode: 0,
Extra: func(t *testing.T, cmd main.AydCommand) {
if cmd.InstanceName != "Test Instance" {
t.Errorf("expected InstanceName is %q but got %q", "Test Instance", cmd.InstanceName)
}
},
},
}

for _, tt := range tests {
Expand Down
28 changes: 18 additions & 10 deletions cmd/ayd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,39 @@ func (cmd *AydCommand) reportStartServer(s *store.Store, protocol, listen string
cmd.StartedAt = time.Now()

u := &api.URL{Scheme: "ayd", Opaque: "server"}
extra := map[string]interface{}{
"url": fmt.Sprintf("%s://%s", protocol, listen),
"targets": tasks,
"version": fmt.Sprintf("%s (%s)", meta.Version, meta.Commit),
}
if s.Name() != "" {
extra["instance_name"] = s.Name()
}
s.Report(u, api.Record{
Time: cmd.StartedAt,
Status: api.StatusHealthy,
Target: u,
Message: "start Ayd server",
Extra: map[string]interface{}{
"url": fmt.Sprintf("%s://%s", protocol, listen),
"targets": tasks,
"version": fmt.Sprintf("%s (%s)", meta.Version, meta.Commit),
},
Extra: extra,
})
}

func (cmd *AydCommand) reportStopServer(s *store.Store, protocol, listen string) {
u := &api.URL{Scheme: "ayd", Opaque: "server"}
extra := map[string]interface{}{
"url": fmt.Sprintf("%s://%s", protocol, listen),
"version": fmt.Sprintf("%s (%s)", meta.Version, meta.Commit),
"since": cmd.StartedAt.Format(time.RFC3339),
}
if s.Name() != "" {
extra["instance_name"] = s.Name()
}
s.Report(u, api.Record{
Time: time.Now(),
Status: api.StatusHealthy,
Target: u,
Message: "stop Ayd server",
Extra: map[string]interface{}{
"url": fmt.Sprintf("%s://%s", protocol, listen),
"version": fmt.Sprintf("%s (%s)", meta.Version, meta.Commit),
"since": cmd.StartedAt.Format(time.RFC3339),
},
Extra: extra,
})
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/ayd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestRunServer_tls(t *testing.T) {
log, stdout := io.Pipe()
defer log.Close()
defer stdout.Close()
s := testutil.NewStoreWithConsole(t, stdout)
s := testutil.NewStore(t, testutil.WithConsole(stdout))
defer s.Close()

cert := testutil.NewCertificate(t)
Expand Down Expand Up @@ -129,7 +129,7 @@ func TestRunServer_tls_error(t *testing.T) {
cmd.CertPath = tt.Cert
cmd.KeyPath = tt.Key

s := testutil.NewStoreWithConsole(t, output)
s := testutil.NewStore(t, testutil.WithConsole(output))

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ require (
github.com/spf13/pflag v1.0.10
github.com/xuri/excelize/v2 v2.10.0
goftp.io/server v0.4.1
golang.org/x/crypto v0.43.0
golang.org/x/crypto v0.44.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.30.0
golang.org/x/text v0.31.0
)

require (
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ goftp.io/server v0.4.1/go.mod h1:hFZeR656ErRt3ojMKt7H10vQ5nuWV1e0YeUTeorlR6k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -96,12 +96,12 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
Expand Down
2 changes: 1 addition & 1 deletion internal/endpoint/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Benchmark_endpoints(b *testing.B) {

for _, tt := range benchmarks {
b.Run(tt.Path, func(b *testing.B) {
s := testutil.NewStoreWithLog(b)
s := testutil.NewStore(b, testutil.WithLog())
defer s.Close()

h := tt.Endpoint(s)
Expand Down
4 changes: 4 additions & 0 deletions internal/endpoint/healthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type DummyErrorsGetter struct {
messages []string
}

func (d DummyErrorsGetter) Name() string {
return "dummy"
}

func (d DummyErrorsGetter) Path() string {
return ""
}
Expand Down
2 changes: 2 additions & 0 deletions internal/endpoint/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ func LogHTMLEndpoint(s Store) http.HandlerFunc {

w.Header().Set("Content-Type", "text/html; charset=UTF-8")
handleError(s, "log.html", tmpl.Execute(newFlushWriter(w), logData{
InstanceName: s.Name(),
Query: query,
RawQuery: rawQuery.Encode(),
Records: rs,
Expand All @@ -382,6 +383,7 @@ func LogHTMLEndpoint(s Store) http.HandlerFunc {
}

type logData struct {
InstanceName string
Since time.Time
Until time.Time
Query string
Expand Down
2 changes: 1 addition & 1 deletion internal/endpoint/log_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func FuzzLogJsonEndpoint(f *testing.F) {
s := testutil.NewStoreWithLog(f)
s := testutil.NewStore(f, testutil.WithLog())
defer s.Close()
handler := endpoint.LogJsonEndpoint(s)

Expand Down
8 changes: 4 additions & 4 deletions internal/endpoint/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func TestLogScanner(t *testing.T) {
{
"LogGenerator",
func(since, until time.Time) api.LogScanner {
s, err := store.New("", io.Discard)
s, err := store.New("", "", io.Discard)
if err != nil {
t.Fatalf("failed to create store: %s", err)
}
Expand Down Expand Up @@ -157,7 +157,7 @@ func TestLogScanner(t *testing.T) {
}

func TestPagingScanner(t *testing.T) {
s := testutil.NewStoreWithLog(t)
s := testutil.NewStore(t, testutil.WithLog())

messages := []string{
"hello world",
Expand Down Expand Up @@ -218,7 +218,7 @@ func TestPagingScanner(t *testing.T) {
}

func TestContextScanner_scanAll(t *testing.T) {
s := testutil.NewStoreWithLog(t)
s := testutil.NewStore(t, testutil.WithLog())

r, err := s.OpenLog(time.Unix(0, 0), time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC))
if err != nil {
Expand All @@ -243,7 +243,7 @@ func TestContextScanner_scanAll(t *testing.T) {
}

func TestContextScanner_cancel(t *testing.T) {
s := testutil.NewStoreWithLog(t)
s := testutil.NewStore(t, testutil.WithLog())

r, err := s.OpenLog(time.Unix(0, 0), time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC))
if err != nil {
Expand Down
20 changes: 16 additions & 4 deletions internal/endpoint/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func jqParseURL(x any, _ []any) any {

if u.Opaque != "" && u.Host == "" {
switch u.Scheme {
case "ping":
case "ping", "ping4", "ping6":
u.Host = u.Opaque
u.Opaque = ""
case "dns", "dns4", "dns6", "file", "exec", "mailto", "source":
Expand Down Expand Up @@ -316,10 +316,22 @@ func FetchLogsByJq(ctx context.Context, s Store, input MCPLogsInput) (output MCP
}

func MCPHandler(s Store) http.HandlerFunc {
server := mcp.NewServer(&mcp.Implementation{
Name: "Ayd",
impl := &mcp.Implementation{
Name: "ayd",
Version: meta.Version,
}, nil)
Title: "Ayd",
}

opts := &mcp.ServerOptions{
Instructions: "Ayd is a simple alive monitoring tool. The logs and status can be large, so it is recommended to extract necessary information using jq queries instead of fetching all data at once.",
}

if s.Name() != "" {
impl.Title = fmt.Sprintf("Ayd (%s)", s.Name())
opts.Instructions = fmt.Sprintf(`%s This Ayd instance's name is %q.`, opts.Instructions, s.Name())
}

server := mcp.NewServer(impl, opts)

mcp.AddTool(server, &mcp.Tool{
Name: "list_targets",
Expand Down
63 changes: 62 additions & 1 deletion internal/endpoint/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -50,7 +51,7 @@ func TestJQQuery(t *testing.T) {
"hostname": "example.com",
"port": "",
"path": "/path",
"queries": map[string][]any{"query": []any{"value"}},
"queries": map[string][]any{"query": {"value"}},
"fragment": "fragment",
"opaque": "",
},
Expand Down Expand Up @@ -624,3 +625,63 @@ func TestMCPHandler_QueryLogs(t *testing.T) {

RunMCPTest(t, "query_logs", tests)
}

func TestMCP_connection(t *testing.T) {
tests := []struct {
Name string
WithInstanceName bool
}{
{"without_instance_name", false},
{"with_instance_name", true},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
var opts []testutil.StoreOption
if tt.WithInstanceName {
opts = append(opts, testutil.WithInstanceName("test-instance"))
}

srv := testutil.StartTestServer(t, opts...)
t.Cleanup(func() {
srv.Close()
})

client := mcp.NewClient(&mcp.Implementation{
Name: "test-client",
Version: "none",
}, nil)
sess, err := client.Connect(t.Context(), &mcp.StreamableClientTransport{
Endpoint: srv.URL + "/mcp",
}, nil)
if err != nil {
t.Fatalf("failed to connect to MCP server: %v", err)
}
defer sess.Close()

initResult := sess.InitializeResult()
if initResult.ServerInfo.Name != "ayd" {
t.Errorf("unexpected server name: %q", initResult.ServerInfo.Name)
}
if tt.WithInstanceName {
if initResult.ServerInfo.Title != "Ayd (test-instance)" {
t.Errorf("unexpected server title: %q", initResult.ServerInfo.Title)
}
if !strings.Contains(initResult.Instructions, "test-instance") {
t.Errorf("instructions does not contain instance name: %q", initResult.Instructions)
}
} else {
if initResult.ServerInfo.Title != "Ayd" {
t.Errorf("unexpected server title: %q", initResult.ServerInfo.Title)
}
if strings.Contains(initResult.Instructions, "instance") {
t.Errorf("instructions contains instance name: %q", initResult.Instructions)
}
}

if err := sess.Ping(t.Context(), nil); err != nil {
t.Fatalf("failed to ping MCP server: %v", err)
}
})
}
}
3 changes: 3 additions & 0 deletions internal/endpoint/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
)

type Store interface {
// Name returns the Ayd instance name.
Name() string

// Targets returns target URLs include inactive target.
Targets() []string

Expand Down
Loading
Loading