Skip to content

Add support for API version 6 #108

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

Merged
merged 5 commits into from
Jul 15, 2022
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ This project includes a client library for working with NGINX Plus API.

## Compatibility

This Client works against version 5 of NGINX Plus API. Version 5 was introduced in NGINX Plus R19.
This Client works against versions 4 to 6 of the NGINX Plus API. The table below shows the version of NGINX Plus where the API was first introduced.

| API version | NGINX Plus version |
|-------------|--------------------|
| 4 | R18 |
| 5 | R19 |
| 6 | R20 |

## Using the Client

Expand Down
151 changes: 121 additions & 30 deletions client/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

const (
// APIVersion is the default version of NGINX Plus API supported by the client.
APIVersion = 5
APIVersion = 6

pathNotFoundCode = "PathNotFound"
streamContext = true
Expand All @@ -25,7 +25,7 @@ const (
)

var (
supportedAPIVersions = versions{4, 5}
supportedAPIVersions = versions{4, 5, 6}

// Default values for servers in Upstreams.
defaultMaxConns = 0
Expand Down Expand Up @@ -116,20 +116,23 @@ func (internalError *internalError) Wrap(err string) *internalError {
// 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 {
NginxInfo NginxInfo
Caches Caches
Processes Processes
Connections Connections
Slabs Slabs
HTTPRequests HTTPRequests
SSL SSL
ServerZones ServerZones
Upstreams Upstreams
StreamServerZones StreamServerZones
StreamUpstreams StreamUpstreams
StreamZoneSync *StreamZoneSync
LocationZones LocationZones
Resolvers Resolvers
NginxInfo NginxInfo
Caches Caches
Processes Processes
Connections Connections
Slabs Slabs
HTTPRequests HTTPRequests
SSL SSL
ServerZones ServerZones
Upstreams Upstreams
StreamServerZones StreamServerZones
StreamUpstreams StreamUpstreams
StreamZoneSync *StreamZoneSync
LocationZones LocationZones
Resolvers Resolvers
HTTPLimitRequests HTTPLimitRequests
HTTPLimitConnections HTTPLimitConnections
StreamLimitConnections StreamLimitConnections
}

// NginxInfo contains general information about NGINX Plus.
Expand Down Expand Up @@ -418,6 +421,31 @@ type Processes struct {
Respawned int64
}

// HTTPLimitRequest represents HTTP Requests Rate Limiting
type HTTPLimitRequest struct {
Passed uint64
Delayed uint64
Rejected uint64
DelayedDryRun uint64 `json:"delayed_dry_run"`
RejectedDryRun uint64 `json:"rejected_dry_run"`
}

// HTTPLimitRequests represents limit requests related stats
type HTTPLimitRequests map[string]HTTPLimitRequest

// LimitConnection represents Connections Limiting
type LimitConnection struct {
Passed uint64
Rejected uint64
RejectedDryRun uint64 `json:"rejected_dry_run"`
}

// HTTPLimitConnections represents limit connections related stats
type HTTPLimitConnections map[string]LimitConnection

// StreamLimitConnections represents limit connections related stats
type StreamLimitConnections map[string]LimitConnection

// NewNginxClient creates an NginxClient with the latest supported version.
func NewNginxClient(httpClient *http.Client, apiEndpoint string) (*NginxClient, error) {
return NewNginxClientWithVersion(httpClient, apiEndpoint, APIVersion)
Expand Down Expand Up @@ -1095,21 +1123,39 @@ func (client *NginxClient) GetStats() (*Stats, error) {
return nil, fmt.Errorf("failed to get stats: %w", err)
}

limitReqs, err := client.GetHTTPLimitReqs()
if err != nil {
return nil, fmt.Errorf("failed to get stats: %w", err)
}

limitConnsHTTP, err := client.GetHTTPConnectionsLimit()
if err != nil {
return nil, fmt.Errorf("failed to get stats: %w", err)
}

limitConnsStream, err := client.GetStreamConnectionsLimit()
if err != nil {
return nil, fmt.Errorf("failed to get stats: %w", err)
}

return &Stats{
NginxInfo: *info,
Caches: *caches,
Processes: *processes,
Slabs: *slabs,
Connections: *cons,
HTTPRequests: *requests,
SSL: *ssl,
ServerZones: *zones,
StreamServerZones: *streamZones,
Upstreams: *upstreams,
StreamUpstreams: *streamUpstreams,
StreamZoneSync: streamZoneSync,
LocationZones: *locationZones,
Resolvers: *resolvers,
NginxInfo: *info,
Caches: *caches,
Processes: *processes,
Slabs: *slabs,
Connections: *cons,
HTTPRequests: *requests,
SSL: *ssl,
ServerZones: *zones,
StreamServerZones: *streamZones,
Upstreams: *upstreams,
StreamUpstreams: *streamUpstreams,
StreamZoneSync: streamZoneSync,
LocationZones: *locationZones,
Resolvers: *resolvers,
HTTPLimitRequests: *limitReqs,
HTTPLimitConnections: *limitConnsHTTP,
StreamLimitConnections: *limitConnsStream,
}, nil
}

Expand Down Expand Up @@ -1500,3 +1546,48 @@ func addPortToServer(server string) string {

return fmt.Sprintf("%v:%v", server, defaultServerPort)
}

// GetHTTPLimitReqs returns http/limit_reqs stats.
func (client *NginxClient) GetHTTPLimitReqs() (*HTTPLimitRequests, error) {
var limitReqs HTTPLimitRequests
if client.version < 6 {
return &limitReqs, nil
}
err := client.get("http/limit_reqs", &limitReqs)
if err != nil {
return nil, fmt.Errorf("failed to get http limit requests: %w", err)
}
return &limitReqs, nil
}

// GetHTTPConnectionsLimit returns http/limit_conns stats.
func (client *NginxClient) GetHTTPConnectionsLimit() (*HTTPLimitConnections, error) {
var limitConns HTTPLimitConnections
if client.version < 6 {
return &limitConns, nil
}
err := client.get("http/limit_conns", &limitConns)
if err != nil {
return nil, fmt.Errorf("failed to get http connections limit: %w", err)
}
return &limitConns, nil
}

// GetStreamConnectionsLimit returns stream/limit_conns stats.
func (client *NginxClient) GetStreamConnectionsLimit() (*StreamLimitConnections, error) {
var limitConns StreamLimitConnections
if client.version < 6 {
return &limitConns, nil
}
err := client.get("stream/limit_conns", &limitConns)
if err != nil {
var ie *internalError
if errors.As(err, &ie) {
if ie.Code == pathNotFoundCode {
return &limitConns, nil
}
}
return nil, fmt.Errorf("failed to get stream connections limit: %w", err)
}
return &limitConns, nil
}
3 changes: 3 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ stream {
keyval_zone zone=zone_one_stream:32k;
keyval $hostname $text zone=zone_one_stream;
keyval_zone zone=zone_test_sync:32k timeout=5s sync;
limit_conn_zone $binary_remote_addr zone=addr_stream:10m;

limit_conn addr_stream 1;

upstream stream_test {
zone stream_test 64k;
Expand Down
5 changes: 5 additions & 0 deletions docker/test.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ upstream test {
}

proxy_cache_path /var/cache/nginx keys_zone=http_cache:10m max_size=100m;
limit_req_zone $binary_remote_addr zone=one:10m rate=1500r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;

server {
listen 8080;

limit_req zone=one burst=100;
limit_conn addr 10;

location = /dashboard.html {
root /usr/share/nginx/html;
}
Expand Down
27 changes: 27 additions & 0 deletions tests/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const (
streamZoneSync = "zone_test_sync"
locationZone = "location_test"
resolverMetric = "resolver_test"
reqZone = "one"
connZone = "addr"
streamConnZone = "addr_stream"
)

var (
Expand Down Expand Up @@ -686,6 +689,22 @@ func TestStats(t *testing.T) {
t.Errorf("Resolver %v not found", resolverMetric)
}

if reqLimit, ok := stats.HTTPLimitRequests[reqZone]; ok {
if reqLimit.Passed < 1 {
t.Errorf("HTTP Reqs limit stats missing: %v", reqLimit.Passed)
}
} else {
t.Errorf("HTTP Reqs limit %v not found", reqLimit)
}

if connLimit, ok := stats.HTTPLimitConnections[connZone]; ok {
if connLimit.Passed < 1 {
t.Errorf("HTTP Limit connections stats missing: %v", connLimit.Passed)
}
} else {
t.Errorf("HTTP Limit connections %v not found", connLimit)
}

// cleanup upstream servers
_, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{})
if err != nil {
Expand Down Expand Up @@ -806,6 +825,14 @@ func TestStreamStats(t *testing.T) {
t.Errorf("Stream upstream 'stream_test' not found")
}

if streamConnLimit, ok := stats.StreamLimitConnections[streamConnZone]; ok {
if streamConnLimit.Passed < 1 {
t.Errorf("Stream Limit connections stats missing: %v", streamConnLimit.Passed)
}
} else {
t.Errorf("Stream Limit connections %v not found", streamConnLimit)
}

// cleanup stream upstream servers
_, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{})
if err != nil {
Expand Down