-
Notifications
You must be signed in to change notification settings - Fork 25
Add stream upstream, stream server zones metrics support #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
3955897
0c34234
2b059ca
6adc4f1
06d8dbe
2745a45
1ae242a
fbe16c5
94ad6a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,24 @@ | ||
NGINX_PLUS_VERSION=15-2 | ||
NGINX_IMAGE=nginxplus:$(NGINX_PLUS_VERSION) | ||
|
||
test: docker-build run-nginx-plus test-run clean | ||
test: docker-build run-nginx-plus test-run config-no-stream test-no-stream clean | ||
|
||
docker-build: | ||
docker build --build-arg NGINX_PLUS_VERSION=$(NGINX_PLUS_VERSION)~stretch -t $(NGINX_IMAGE) docker | ||
|
||
run-nginx-plus: | ||
docker run -d --name nginx-plus-test --rm -p 8080:8080 $(NGINX_IMAGE) | ||
docker run -d --name nginx-plus-test --rm -p 8080:8080 -p 8081:8081 $(NGINX_IMAGE) | ||
|
||
test-run: | ||
go test client/* | ||
go test tests/client_test.go | ||
GOCACHE=off go test client/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for unit tests in client, don't see why we need to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without it, changes in templates won't get tested. Because the go code doesn't change, the test results will be cached. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for this ones https://github.com/nginxinc/nginx-plus-go-sdk/blob/master/client/nginx_test.go we don't need any template There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ugh alright |
||
GOCACHE=off go test tests/client_test.go | ||
|
||
config-no-stream: | ||
docker cp docker/nginx_no_stream.conf nginx-plus-test:/etc/nginx/nginx.conf | ||
docker exec nginx-plus-test nginx -s reload | ||
|
||
test-no-stream: | ||
GOCACHE=off go test tests/client_no_stream_test.go | ||
|
||
clean: | ||
docker kill nginx-plus-test |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ import ( | |
// APIVersion is a version of NGINX Plus API. | ||
const APIVersion = 2 | ||
|
||
const streamNotConfiguredCode = "StreamNotConfigured" | ||
|
||
// NginxClient lets you access NGINX Plus API. | ||
type NginxClient struct { | ||
apiEndpoint string | ||
|
@@ -57,14 +59,25 @@ type apiError struct { | |
Code string | ||
} | ||
|
||
type internalError struct { | ||
apiError | ||
isaachawley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
err string | ||
isaachawley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func (internalError *internalError) Error() string { | ||
return internalError.err | ||
} | ||
|
||
// Stats represents NGINX Plus stats fetched from the NGINX Plus API. | ||
// https://nginx.org/en/docs/http/ngx_http_api_module.html | ||
type Stats struct { | ||
Connections Connections | ||
HTTPRequests HTTPRequests | ||
SSL SSL | ||
ServerZones ServerZones | ||
Upstreams Upstreams | ||
Connections Connections | ||
HTTPRequests HTTPRequests | ||
SSL SSL | ||
ServerZones ServerZones | ||
Upstreams Upstreams | ||
StreamServerZones StreamServerZones | ||
StreamUpstreams StreamUpstreams | ||
} | ||
|
||
// Connections represents connection related stats. | ||
|
@@ -101,7 +114,20 @@ type ServerZone struct { | |
Sent uint64 | ||
} | ||
|
||
// Responses represents HTTP reponse related stats. | ||
// StreamServerZones is map of stream server zone stats by zone name. | ||
type StreamServerZones map[string]StreamServerZone | ||
|
||
// StreamServerZone represents stream server zone related stats. | ||
type StreamServerZone struct { | ||
Processing uint64 | ||
Connections uint64 | ||
Sessions Sessions | ||
Discarded uint64 | ||
Received uint64 | ||
Sent uint64 | ||
} | ||
|
||
// Responses represents HTTP response related stats. | ||
type Responses struct { | ||
Responses1xx uint64 `json:"1xx"` | ||
Responses2xx uint64 `json:"2xx"` | ||
|
@@ -111,6 +137,14 @@ type Responses struct { | |
Total uint64 | ||
} | ||
|
||
// Sessions represents stream session related stats. | ||
type Sessions struct { | ||
Sessions2xx uint64 `json:"2xx"` | ||
Sessions4xx uint64 `josn:"4xx"` | ||
Sessions5xx uint64 `josn:"5xx"` | ||
Total uint64 | ||
} | ||
|
||
// Upstreams is a map of upstream stats by upstream name. | ||
type Upstreams map[string]Upstream | ||
|
||
|
@@ -123,6 +157,16 @@ type Upstream struct { | |
Queue Queue | ||
} | ||
|
||
// StreamUpstreams is a map of stream upstream stats by upstream name. | ||
type StreamUpstreams map[string]StreamUpstream | ||
|
||
// StreamUpstream represents stream upstream related stats. | ||
type StreamUpstream struct { | ||
Peers []StreamPeer | ||
Zombies int | ||
Zone string | ||
} | ||
|
||
// Queue represents queue related stats for an upstream. | ||
type Queue struct { | ||
Size int | ||
|
@@ -155,6 +199,31 @@ type Peer struct { | |
ResponseTime uint64 `json:"response_time"` | ||
} | ||
|
||
// StreamPeer represents peer (stream upstream server) related stats. | ||
type StreamPeer struct { | ||
ID int | ||
Server string | ||
Service string | ||
Name string | ||
Backup bool | ||
Weight int | ||
State string | ||
Active uint64 | ||
MaxConns int `json:"max_conns"` | ||
Connections uint64 | ||
ConnectTime int `json:"connect_time"` | ||
FirstByteTime int `json:"first_byte_time"` | ||
ResponseTime uint64 `json:"response_time"` | ||
Sent uint64 | ||
Received uint64 | ||
Fails uint64 | ||
Unavail uint64 | ||
HealthChecks HealthChecks `json:"health_checks"` | ||
Downtime uint64 | ||
Downstart string | ||
Selected string | ||
} | ||
|
||
// HealthChecks represents health check related stats for a peer. | ||
type HealthChecks struct { | ||
Checks uint64 | ||
|
@@ -215,12 +284,15 @@ func getAPIVersions(httpClient *http.Client, endpoint string) (*versions, error) | |
} | ||
|
||
func createResponseMismatchError(respBody io.ReadCloser, mainErr error) error { | ||
apiErr, err := readAPIErrorResponse(respBody) | ||
apiErrResp, err := readAPIErrorResponse(respBody) | ||
if err != nil { | ||
return fmt.Errorf("%v; failed to read the response body: %v", mainErr, err) | ||
} | ||
|
||
return fmt.Errorf("%v; error: %v", mainErr, apiErr.toString()) | ||
return &internalError{ | ||
err: fmt.Sprintf("%v; error: %v", mainErr, apiErrResp.toString()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to store the mainErr inside internalError, as the apiErr that we get from Plus describes the problem very well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the same thing it did before see master. |
||
apiError: apiErrResp.Error, | ||
} | ||
} | ||
|
||
func readAPIErrorResponse(respBody io.ReadCloser) (*apiErrorResponse, error) { | ||
|
@@ -458,7 +530,7 @@ func (client *NginxClient) GetStreamServers(upstream string) ([]StreamUpstreamSe | |
return servers, nil | ||
} | ||
|
||
// AddStreamServer adds the server to the upstream. | ||
// AddStreamServer adds the stream server to the upstream. | ||
func (client *NginxClient) AddStreamServer(upstream string, server StreamUpstreamServer) error { | ||
id, err := client.getIDOfStreamServer(upstream, server.Server) | ||
|
||
|
@@ -572,7 +644,7 @@ func determineStreamUpdates(updatedServers []StreamUpstreamServer, nginxServers | |
return | ||
} | ||
|
||
// GetStats gets connection, request, ssl, zone, and upstream related stats from the NGINX Plus API. | ||
// GetStats gets connection, request, ssl, zone, stream zone, upstream and stream upstream related stats from the NGINX Plus API. | ||
func (client *NginxClient) GetStats() (*Stats, error) { | ||
cons, err := client.getConnections() | ||
if err != nil { | ||
|
@@ -594,17 +666,29 @@ func (client *NginxClient) GetStats() (*Stats, error) { | |
return nil, fmt.Errorf("failed to get stats: %v", err) | ||
} | ||
|
||
streamZones, err := client.getStreamServerZones() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please move it after getUpstreams call |
||
if err != nil { | ||
return nil, fmt.Errorf("failed to get stats: %v", err) | ||
} | ||
|
||
upstreams, err := client.getUpstreams() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get stats: %v", err) | ||
} | ||
|
||
streamUpstreams, err := client.getStreamUpstreams() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get stats: %v", err) | ||
} | ||
|
||
return &Stats{ | ||
Connections: *cons, | ||
HTTPRequests: *requests, | ||
SSL: *ssl, | ||
ServerZones: *zones, | ||
Upstreams: *upstreams, | ||
Connections: *cons, | ||
HTTPRequests: *requests, | ||
SSL: *ssl, | ||
ServerZones: *zones, | ||
StreamServerZones: *streamZones, | ||
Upstreams: *upstreams, | ||
StreamUpstreams: *streamUpstreams, | ||
}, nil | ||
} | ||
|
||
|
@@ -646,6 +730,20 @@ func (client *NginxClient) getServerZones() (*ServerZones, error) { | |
return &zones, err | ||
} | ||
|
||
func (client *NginxClient) getStreamServerZones() (*StreamServerZones, error) { | ||
var zones StreamServerZones | ||
err := client.get("stream/server_zones", &zones) | ||
if err != nil { | ||
isaachawley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err, ok := err.(*internalError); ok { | ||
if err.Code == streamNotConfiguredCode { | ||
return &zones, nil | ||
} | ||
} | ||
return nil, fmt.Errorf("failed to get stream server zones: %v", err) | ||
} | ||
return &zones, err | ||
} | ||
|
||
func (client *NginxClient) getUpstreams() (*Upstreams, error) { | ||
var upstreams Upstreams | ||
err := client.get("http/upstreams", &upstreams) | ||
|
@@ -654,3 +752,17 @@ func (client *NginxClient) getUpstreams() (*Upstreams, error) { | |
} | ||
return &upstreams, nil | ||
} | ||
|
||
func (client *NginxClient) getStreamUpstreams() (*StreamUpstreams, error) { | ||
var upstreams StreamUpstreams | ||
err := client.get("stream/upstreams", &upstreams) | ||
if err != nil { | ||
isaachawley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err, ok := err.(*internalError); ok { | ||
if err.Code == streamNotConfiguredCode { | ||
return &upstreams, nil | ||
} | ||
} | ||
return nil, fmt.Errorf("failed to get stream upstreams: %v", err) | ||
} | ||
return &upstreams, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
|
||
user nginx; | ||
worker_processes auto; | ||
|
||
error_log /var/log/nginx/error.log notice; | ||
pid /var/run/nginx.pid; | ||
|
||
|
||
events { | ||
worker_connections 1024; | ||
} | ||
|
||
|
||
http { | ||
include /etc/nginx/mime.types; | ||
default_type application/octet-stream; | ||
|
||
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' | ||
'$status $body_bytes_sent "$http_referer" ' | ||
'"$http_user_agent" "$http_x_forwarded_for"'; | ||
|
||
access_log /var/log/nginx/access.log main; | ||
|
||
sendfile on; | ||
#tcp_nopush on; | ||
|
||
keepalive_timeout 65; | ||
|
||
#gzip on; | ||
|
||
include /etc/nginx/conf.d/*.conf; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,4 +22,4 @@ server { | |
health_check interval=10 fails=3 passes=1; | ||
} | ||
status_zone test; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package tests | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/nginxinc/nginx-plus-go-sdk/client" | ||
) | ||
|
||
// TestStatsNoStream tests the peculiar behavior of getting Stream-related | ||
// stats from the API when there are no stream blocks in the config. | ||
// The API returns a special error code that we can use to determine if the API | ||
// is misconfigured or of the stream block is missing. | ||
func TestStatsNoStream(t *testing.T) { | ||
httpClient := &http.Client{} | ||
c, err := client.NewNginxClient(httpClient, "http://127.0.0.1:8080/api") | ||
if err != nil { | ||
t.Fatalf("Error connecting to nginx: %v", err) | ||
} | ||
|
||
stats, err := c.GetStats() | ||
if err != nil { | ||
t.Errorf("Error getting stats: %v", err) | ||
} | ||
|
||
if stats.Connections.Active == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we make this check? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a sanity check. We should get some connections from the api even without the stream block. I'll make it clearer. |
||
t.Errorf("Bad connections: %v", stats.Connections) | ||
} | ||
|
||
if len(stats.StreamServerZones) != 0 { | ||
t.Error("No stream block should result in no StreamServerZones") | ||
} | ||
|
||
if len(stats.StreamUpstreams) != 0 { | ||
t.Error("No stream block should result in no StreamUpstreams") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we remove the targets
config-no-stream
andtest-no-stream
and put all that config manipulation in thetest-run
target? I think it will make it less confusingThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it this way, so that we can change the config and
make test-run
ormake test-no-stream
without changing the state of the container.I also don't find this confusing.