Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b9a313a
feat(endpoint/mcp): support remote MCP server (#44)
macrat Nov 17, 2025
404ba09
feat: implement Instance Name feature (#48)
macrat Nov 22, 2025
f238df3
refactor(MCP): refactor MCP code
macrat Nov 22, 2025
905e821
test(MCP): add benchmark for query_logs tool
macrat Nov 22, 2025
fe8a8ad
feat(MCP): remove inefficient tool and improve tool descriptions
macrat Nov 22, 2025
9815feb
feat(MCP): improve MCP tools for better results with less resources
macrat Nov 22, 2025
eefcea9
test(MCP): make too large test smaller
macrat Nov 22, 2025
f94d562
perf(lib): improve performance of ParseTime
macrat Nov 22, 2025
acd165d
style: run formatter
macrat Nov 23, 2025
014e821
perf(lib): remove unnecessary invalid UTF guard in Record.UnmarshalJSON
macrat Nov 23, 2025
b8fcbf6
perf(store): optimize memory allocation in log file scanner
macrat Nov 23, 2025
fbbd953
test(lib): add some tests for time parsing
macrat Nov 24, 2025
358be2f
perf(store): improve log writer
macrat Nov 24, 2025
bef5b6b
perf(lib): improve time parser for speed
macrat Nov 29, 2025
ed5d455
test(query): add benchmark for query
macrat Nov 29, 2025
b51c92f
perf(query): optimize query parser and matcher
macrat Nov 29, 2025
237d2a6
fix(lib): avoid Record.MarshalJSON generate invalid json
macrat Nov 29, 2025
f6d1951
test(lib): add fuzzing test to parse time
macrat Nov 29, 2025
9d304e9
perf(query): optimize time parser in query
macrat Nov 29, 2025
35dedd5
test(lib): remove duplicated test
macrat Nov 29, 2025
403ee9f
test(lib): fix fuzzing test bug
macrat Nov 29, 2025
4e18723
fix(lib): add unicode validator when parsing plugin outputs
macrat Nov 29, 2025
8b1a969
fix(endpoint): fix an issue that potentialy crashes HTML pages
macrat Nov 29, 2025
a0179b1
fix(store): order of time patterns
macrat Nov 29, 2025
f76b72c
fix(endpoint): fix wrong chunk size from HTTP endpoints
macrat Nov 29, 2025
dea3c6f
fix(endpoint): prevent to generate invalid SVG in status page
macrat Nov 29, 2025
053f094
fix(cmd): make negative or zero interval as error
macrat Nov 29, 2025
c037ae5
fix(endpoint): fix error message scope in /incidents.json
macrat Nov 29, 2025
b854450
fix(endpoint): fix target URL escape in /metrics
macrat Nov 29, 2025
f774cd5
fix(store): fix issue that potentialy create empty log file while rea…
macrat Nov 29, 2025
b8e331f
fix(logconv): improve error handling for csv converter
macrat Nov 29, 2025
690b825
perf(endpoint/mcp): change timing to parse query for improve log read…
macrat Nov 29, 2025
89f78e0
fix(scheme/ssh): improve error handling for invalid ssh/sftp URL
macrat Nov 29, 2025
ef9a05f
test(scheme/dns): follow change of example.com hosting service
macrat Dec 29, 2025
ab89855
chore(deps): update dependencies
macrat Jan 23, 2026
2790e21
Merge branch 'main' into v0.18
macrat Jan 23, 2026
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
8 changes: 4 additions & 4 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ builds:
- -trimpath
ldflags:
- '-s -w'
- '-X main.version={{ .Version }}'
- '-X main.commit={{ .ShortCommit }}'
- '-X github.com/macrat/ayd/internal/meta.Version={{ .Version }}'
- '-X github.com/macrat/ayd/internal/meta.Commit={{ .ShortCommit }}'
hooks:
post: 'upx-ucl --lzma {{ .Path }}'
- id: without-upx
Expand All @@ -32,8 +32,8 @@ builds:
- -trimpath
ldflags:
- '-s -w'
- '-X main.version={{ .Version }}'
- '-X main.commit={{ .ShortCommit }}'
- '-X github.com/macrat/ayd/internal/meta.Version={{ .Version }}'
- '-X github.com/macrat/ayd/internal/meta.Commit={{ .ShortCommit }}'

archives:
- formats: [tar.gz]
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ RUN for x in $PLUGINS; do \

COPY . /usr/src/ayd/
RUN cd /usr/src/ayd/cmd/ayd && \
CGO_ENABLED=0 go build --trimpath -ldflags="-s -w -X 'main.version=$VERSION' -X 'main.commit=$COMMIT'" -buildvcs=false -o /output/ayd
CGO_ENABLED=0 go build --trimpath -ldflags="-s -w -X 'github.com/macrat/ayd/internal/meta.Version=$VERSION' -X 'github.com/macrat/ayd/internal/meta.Commit=$COMMIT'" -buildvcs=false -o /output/ayd

RUN upx-ucl --lzma /output/*

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ COMMIT = $(shell git rev-parse --short $(shell git describe))


ayd: ${SOURCES}
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" -trimpath -o ayd ./cmd/ayd
CGO_ENABLED=0 go build -ldflags="-s -w -X github.com/macrat/ayd/internal/meta.Version=${VERSION} -X github.com/macrat/ayd/internal/meta.Commit=${COMMIT}" -trimpath -o ayd ./cmd/ayd


.PHONY: test containertest cover fmt resources clean install
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ $ ayd ping:192.168.1.1 https://example.com

- The [status page](#status-pages-and-endpoints) for using by browsers, consoles, or other programs.

- Built-in remote MCP server for AI tools like Claude or ChatGPT to analyze the status and logs.

- Send an alert if an incident occurs or is resolved.

### Good at
Expand Down Expand Up @@ -611,6 +613,7 @@ Ayd has these pages/endpoints.
| [/log.json](http://localhost:9000/log.json) | Raw log file in JSON format. |
| [/targets.txt](http://localhost:9000/targets.txt) | The list of target URLs, separated by \\n. |
| [/targets.json](http://localhost:9000/targets.json) | The list of target URLs in JSON format. |
| [/mcp](http://localhost:9000/mcp) | Remote [MCP](https://modelcontextprotocol.io/docs/getting-started/intro) server endpoint.|
| [/metrics](http://localhost:9000/metrics) | Minimal status page for use by [Prometheus](https://prometheus.io/). |
| [/healthz](http://localhost:9000/healthz) | Health status page for checking status of Ayd itself. |

Expand Down Expand Up @@ -641,6 +644,13 @@ Query examples:
- `status!=healthy target=ping:*`: The logs within recent 7 days that only about unhealthy ping targets.


#### MCP server

Ayd supports [MCP (Model Context Protocol)](https://modelcontextprotocol.io/docs/getting-started/intro) for AI tools like Claude or ChatGPT to analyze the status and logs.

To use MCP server, simply run Ayd as usual and add `http://localhost:9000/mcp` as a remote MCP server URL to your AI tool.


### Log file

The log file of Ayd is stored in [JSON Lines](https://jsonlines.org/) format, encoded UTF-8.
Expand Down
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
36 changes: 15 additions & 21 deletions cmd/ayd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,27 @@ import (
"text/template"
"time"

"github.com/macrat/ayd/internal/meta"
"github.com/macrat/ayd/internal/scheme"
"github.com/macrat/ayd/internal/store"
api "github.com/macrat/ayd/lib-ayd"
"github.com/spf13/pflag"
)

var (
version = "HEAD"
commit = "UNKNOWN"
)

func init() {
scheme.HTTPUserAgent = fmt.Sprintf("ayd/%s health check", 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 All @@ -53,7 +46,7 @@ var helpText string
func (cmd *AydCommand) PrintUsage(detail bool) {
tmpl := template.Must(template.New("help.txt").Parse(helpText))
tmpl.Execute(cmd.ErrStream, map[string]interface{}{
"Version": version,
"Version": meta.Version,
"HTTPRedirectMax": scheme.HTTP_REDIRECT_MAX,
"Short": !detail,
})
Expand All @@ -64,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 @@ -119,7 +113,7 @@ func (cmd *AydCommand) ParseArgs(args []string) (exitCode int) {
}

func (cmd *AydCommand) PrintVersion() {
fmt.Fprintf(cmd.OutStream, "Ayd version %s (%s)\n", version, commit)
fmt.Fprintf(cmd.OutStream, "Ayd version %s (%s)\n", meta.Version, meta.Commit)
}

func (cmd *AydCommand) Run(args []string) (exitCode int) {
Expand All @@ -137,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
2 changes: 2 additions & 0 deletions cmd/ayd/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type IntervalSchedule struct {
func ParseIntervalSchedule(spec string) (IntervalSchedule, error) {
if d, err := time.ParseDuration(spec); err != nil {
return IntervalSchedule{}, err
} else if d <= 0 {
return IntervalSchedule{}, fmt.Errorf("interval duration: %q", spec)
} else {
return IntervalSchedule{d}, nil
}
Expand Down
27 changes: 27 additions & 0 deletions cmd/ayd/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ import (
"github.com/macrat/ayd/cmd/ayd"
)

func TestParseIntervalSchedule(t *testing.T) {
tests := []struct {
Name string
Input string
Output time.Duration
Error string
}{
{"valid", "10s", 10 * time.Second, ""},
{"invalid", "abc", 0, "time: invalid duration \"abc\""},
{"zero", "0h", 0, "interval duration: \"0h\""},
{"negative", "-5m", 0, "interval duration: \"-5m\""},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
schedule, err := main.ParseIntervalSchedule(tt.Input)
if err != nil && err.Error() != tt.Error {
t.Fatalf("unexpected error: expected %#v but got %#v", tt.Error, err.Error())
}

if schedule.Interval != tt.Output {
t.Errorf("expected %#v but got %#v", tt.Output, schedule.Interval)
}
})
}
}

func TestParseCronSchedule(t *testing.T) {
tests := []struct {
Name string
Expand Down
36 changes: 24 additions & 12 deletions cmd/ayd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/macrat/ayd/internal/endpoint"
"github.com/macrat/ayd/internal/meta"
"github.com/macrat/ayd/internal/store"
api "github.com/macrat/ayd/lib-ayd"
"github.com/robfig/cron/v3"
Expand All @@ -32,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)", version, 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)", version, commit),
"since": cmd.StartedAt.Format(time.RFC3339),
},
Extra: extra,
})
}

Expand Down Expand Up @@ -136,8 +145,11 @@ func (cmd *AydCommand) RunServer(ctx context.Context, s *store.Store) (exitCode
wg.Done()
}()

if err := srv.Shutdown(context.Background()); err != nil {
s.ReportInternalError("endpoint", err.Error())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
s.ReportInternalError("endpoint", fmt.Sprintf("failed to gracefully shutdown: %s", err.Error()))
}
wg.Done()
}()
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
20 changes: 13 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,33 @@ require (
github.com/goccy/go-json v0.10.5
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/itchyny/gojq v0.12.18
github.com/jlaffaye/ftp v0.2.0
github.com/macrat/go-parallel-pinger v1.1.6
github.com/mattn/go-isatty v0.0.20
github.com/modelcontextprotocol/go-sdk v1.2.0
github.com/pkg/sftp v1.13.10
github.com/robfig/cron/v3 v3.0.1
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.46.0
golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0
golang.org/x/crypto v0.47.0
golang.org/x/sys v0.40.0
golang.org/x/text v0.33.0
)

require (
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/itchyny/timefmt-go v0.1.7 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/richardlehane/mscfb v1.0.6 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
golang.org/x/net v0.47.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
)
Loading
Loading