Skip to content
1 change: 1 addition & 0 deletions changelogs/9.0.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ https://github.com/elastic/apm-server/compare/v\...v9.0.0[View commits]
[float]
==== Breaking Changes
- Change `sampling.tail.storage_limit` default to `0`. While `0` means unlimited local tail-sampling database size, it now enforces a max 80% disk usage on the disk where the data directory is located. Any tail sampling writes after this threshold will be rejected, similar to what happens when tail-sampling database size exceeds a non-0 storage limit. Setting `sampling.tail.storage_limit` to non-0 maintains the existing behavior which limits the tail-sampling database size to `sampling.tail.storage_limit` and does not have the new disk usage threshold check. {pull}15467[15467] {pull}15524[15524]
- Change health check endpoint `/` to only accept GET and HEAD requests, otherwise return HTTP 405 Method Not Allowed.

[float]
==== Deprecations
Expand Down
57 changes: 51 additions & 6 deletions internal/beater/api/mux_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/apm-server/internal/beater/config"
"github.com/elastic/apm-server/internal/beater/headers"
"github.com/elastic/apm-server/internal/beater/monitoringtest"
"github.com/elastic/apm-server/internal/beater/request"
"github.com/elastic/apm-server/internal/version"
)
Expand Down Expand Up @@ -63,10 +65,53 @@ func TestRootHandler_PanicMiddleware(t *testing.T) {
}

func TestRootHandler_MonitoringMiddleware(t *testing.T) {
testMonitoringMiddleware(t, "/", map[string]any{
"http.server." + string(request.IDRequestCount): 1,
"http.server." + string(request.IDResponseCount): 1,
"http.server." + string(request.IDResponseValidCount): 1,
"http.server." + string(request.IDResponseValidOK): 1,
})
validMethodMetrics := map[string]any{
"http.server." + string(request.IDRequestCount): 1,
"http.server." + string(request.IDResponseCount): 1,
"http.server." + string(request.IDResponseValidCount): 1,
"http.server." + string(request.IDResponseValidOK): 1,
"apm-server.root." + string(request.IDRequestCount): 1,
"apm-server.root." + string(request.IDResponseCount): 1,
"apm-server.root." + string(request.IDResponseValidCount): 1,
"apm-server.root." + string(request.IDResponseValidOK): 1,
}
invalidMethodMetrics := map[string]any{
"http.server." + string(request.IDRequestCount): 1,
"http.server." + string(request.IDResponseCount): 1,
"http.server." + string(request.IDResponseErrorsCount): 1,
"http.server." + string(request.IDResponseErrorsMethodNotAllowed): 1,
"apm-server.root." + string(request.IDRequestCount): 1,
"apm-server.root." + string(request.IDResponseCount): 1,
"apm-server.root." + string(request.IDResponseErrorsCount): 1,
"apm-server.root." + string(request.IDResponseErrorsMethodNotAllowed): 1,
}
for _, tc := range []struct {
method string
wantMetrics map[string]any
}{
{
method: http.MethodGet,
wantMetrics: validMethodMetrics,
},
{
method: http.MethodHead,
wantMetrics: validMethodMetrics,
},
{
method: http.MethodPut,
wantMetrics: invalidMethodMetrics,
},
{
method: http.MethodPost,
wantMetrics: invalidMethodMetrics,
},
} {
t.Run(tc.method, func(t *testing.T) {
h, reader := newTestMux(t, config.DefaultConfig())
req := httptest.NewRequest(tc.method, "/", nil)
h.ServeHTTP(httptest.NewRecorder(), req)

monitoringtest.ExpectContainOtelMetrics(t, reader, tc.wantMetrics)
})
}
}
17 changes: 15 additions & 2 deletions internal/beater/api/root/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package root

import (
"errors"
"net/http"
"time"

"github.com/elastic/beats/v7/libbeat/version"
Expand All @@ -38,10 +40,21 @@ type HandlerConfig struct {

// Handler returns error if route does not exist,
// otherwise returns information about the server. The detail level differs for authenticated and anonymous requests.
// TODO: only allow GET, HEAD requests (breaking change)
func Handler(cfg HandlerConfig) request.Handler {

return func(c *request.Context) {
// Only allow GET, HEAD requests
switch c.Request.Method {
case http.MethodGet, http.MethodHead:
default:
c.Result.SetWithError(
request.IDResponseErrorsMethodNotAllowed,
// include a verbose error message to alert users about a common misconfiguration
errors.New("this is the health check endpoint; did you mean to send data to another endpoint instead?"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[to reviewer] lmk if this is too verbose, I can revert the commit.

$ curl -XPOST -v localhost:8200/
* Host localhost:8200 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8200...
* connect to ::1 port 8200 from ::1 port 49848 failed: Connection refused
*   Trying 127.0.0.1:8200...
* Connected to localhost (127.0.0.1) port 8200
> POST / HTTP/1.1
> Host: localhost:8200
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 405 Method Not Allowed
< Content-Type: application/json
< X-Content-Type-Options: nosniff
< Date: Mon, 03 Mar 2025 16:35:38 GMT
< Content-Length: 129
< 
{
  "error": "method not supported: this is the health check endpoint; did you mean to send data to another endpoint instead?"
}
* Connection #0 to host localhost left intact

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine.

)
c.WriteResult()
return
}

serverInfo := mapstr.M{
"build_date": version.BuildTime().Format(time.RFC3339),
"build_sha": version.Commit(),
Expand Down
Loading