Sending Nginx Access Logs to a Golang Server over syslog

Tufin
4 min readMar 17, 2021

By Effi Bar-She’an and some other guy

Steps:

  1. Writing a Golang UDP server
  2. Testing our UDP server using netcat (nc), a utility for reading from and writing to network connections using UDP (or TCP)
  3. Configure and run nginx to send access-logs to our Golang server
  4. Create a custom access-log format
  5. Parsing access-log json sent from nginx

Writing a Go UDP server

package main

import (
"context"
"net"

log "github.com/sirupsen/logrus"
)

func main() {

if err := serve(context.Background(), ":6060"); err != nil {
os.Exit(1)
}
}

// serve is capable of answering to a single client at a time
func serve(ctx context.Context, address string) error {

pc, err := net.ListenPacket("udp", address)
if err != nil {
log.Errorf("failed to UDP listen on '%s' with '%v'", address, err)
return err
}
defer func() {
if err := pc.Close(); err != nil {
log.Errorf("failed to close packet connection with '%v'", err)
}
}()

doneChan := make(chan error, 1)
// maxBufferSize specifies the size of the buffers that
// are used to temporarily hold data from the UDP packets
// that we receive.
buffer := make([]byte, 1024)

go func() {
for {
n, addr, err := pc.ReadFrom(buffer)
if err != nil {
doneChan <- err
return err
}
log.Infof("address: '%+v' bytes: '%d' request: '%s'", addr, n, string(buffer))
}
}()

var ret error
select {
case <-ctx.Done():
ret = ctx.Err()
log.Infof("cancelled with '%v'", err)
case ret = <-errChan:
}

return ret
}

Testing our UDP server

In order to test our UDP server on mac we can use netcat (more info about it you can find in here):

$ nc -u localhost 6060

Now, we are connected to our UDP server running on localhost on port 6060, so writing on the terminal will send packets to the server and vice versa. Mean, writing, for example “hola” on the terminal should result with a log in our Go server.

Tip, you can also use netstat to check if our server listen on udp:

$ netstat -anv | grep 6060

Configure and run nginx

Let’s start with a simple ngnix config that listens on port 8085, functions as a reverse proxy and will write logs to the current directory:

events {}

http {
server {
listen 8085;
access_log /tmp/logs/nginx-access.log
location / {
proxy_pass http://host.docker.internal:8081;
}
}
}

The easiest way to run nginx is using docker:

$ docker run -it --rm -v $PWD/nginx.conf:/etc/nginx/nginx.conf -v $PWD:/tmp/logs -p 8085:8085 nginx:1.19-alpine

We configured the requests to be forwarded to a backend server on port 8081. Note, that if you’re doing so on localhost use host.docker.internal, like we did above.

Now, we are going to call our server and see if it creates an access-log:

$ curl localhost:8085/hello

If you didn’t create a service that listens on localhost:8081 it will return 502 Bad Gateway. What is more interesting is to look at the new log file nginx-access.log in current-directory? Does it contain your request? If so, great, let’s move on :)

Next, we are going to configure our access_log to be sent to our UDP server by modifying nginx config file and rerunning nginx server (more data regarding configuring logging you can find here)

events {}

http {
server {
listen 8085;
access_log syslog:server=host.docker.internal:6060,facility=local7,tag=nginx,severity=info;
location / {
proxy_pass http://host.docker.internal:8081;
proxy_set_header X-Connection $connection;
}
}
}

After running our Go UDP server, let’s call /hello again using curl and this time we should see that the access log is written as a Go log :)

Create a custom log format

Now, let’s change the format of the access-log to json so it’ll be easier to parse it on the Go server:

events {}

http {
log_format tufin escape=json
'{'
'"time":"$msec",'
'"connection":"$connection",'
'"request":"$request",'
'"status":"$status",'
'"user_agent":"$http_user_agent"'
'}';

server {
listen 8085;
access_log syslog:server=host.docker.internal:6060,facility=local7,tag=nginx,severity=info tufin;
location / {
proxy_pass http://host.docker.internal:8081;
}
}
}

As you can see added a log_format definition name tufin and we are using it in the server’s access_log. You can find out more here.

Parse Access-Log sent from Nginx

type accessLog struct {
MethodPathProtocol string `json:"request"`
StatusCode string `json:"status_code"`
Connection string `json:"connection"`
}

func toAccessLog(accessLogRequest []byte) (*accessLog, error) {

const substr = `{"time":`
start := strings.Index(string(accessLogRequest), substr)
if start < 0 {
msg := fmt.Sprintf("failed to find access-log request JSON '%s' starting with '%s'", string(accessLogRequest), substr)
log.Errorf(msg)
return nil, errors.New(msg)
}
var ret accessLog
err := json.Unmarshal(bytes.Trim([]byte(string(accessLogRequest)[start:]), "\x00"), &ret)
if err != nil {
log.Errorf("failed to unmarshal access-log '%s' with '%v'", string(accessLogRequest)[start:], err)
return nil, err
}

return &ret, nil
}

Notes:

  • Nginx adds some prefix before the json that we defined in the previous section, that is why we start by getting request substring json
  • We have to trim “\x00” otherwise it fail to unmarshal json

You can find the full code in GitHub :)

--

--

Tufin

From the Security Policy Company. This blog is dedicated to cloud-native topics such as Kubernetes, cloud security and micro-services.