# Copyright 2014, Scalyr, Inc.
#
# Note, this can be run in standalone mode by:
# python -m scalyr_agent.run_monitor
# scalyr_agent.builtin_monitors.nginx_monitor
from __future__ import unicode_literals
from __future__ import absolute_import

import socket

import six
import six.moves.http_client
import six.moves.urllib.request
import six.moves.urllib.error
import six.moves.urllib.parse

from scalyr_agent.scalyr_monitor import (
    ScalyrMonitor,
    define_metric,
    define_log_field,
    define_config_option,
)

httpSourceAddress = "127.0.0.1"

__monitor__ = __name__

define_config_option(
    __monitor__,
    "module",
    "Always ``scalyr_agent.builtin_monitors.nginx_monitor``",
    convert_to=six.text_type,
    required_option=True,
)
define_config_option(
    __monitor__,
    "status_url",
    "Optional (defaults to 'http://localhost/nginx_status').  The URL the monitor will fetch"
    "to retrieve the nginx status information.",
    default="http://localhost/nginx_status",
)
define_config_option(
    __monitor__,
    "source_address",
    "Optional (defaults to '%s'). The IP address to be used as the source address when fetching "
    "the status URL.  Many servers require this to be 127.0.0.1 because they only server the status "
    "page to requests from localhost." % httpSourceAddress,
    default=httpSourceAddress,
)
define_config_option(
    __monitor__,
    "id",
    "Optional (defaults to empty string).  Included in each log message generated by this monitor, "
    "as a field named ``instance``. Allows you to distinguish between different nginx instances "
    "running on the same server.",
    convert_to=six.text_type,
)

define_log_field(__monitor__, "monitor", "Always ``nginx_monitor``.")
define_log_field(
    __monitor__,
    "metric",
    "The metric name.  See the metric tables for more information.",
)
define_log_field(__monitor__, "value", "The value of the metric.")
define_log_field(
    __monitor__, "instance", "The ``id`` value from the monitor configuration."
)

define_metric(
    __monitor__,
    "nginx.connections.active",
    "This is the number of connections currently opened to the "
    "server.  The total number of allowed connections is a function "
    "of the number of worker_processes and the number of "
    "worker_connections configured within your Nginx configuration "
    "file.",
)
define_metric(
    __monitor__,
    "nginx.connections.reading",
    "The number of connections currently reading from the clients.",
)
define_metric(
    __monitor__,
    "nginx.connections.writing",
    "The number of connections currently writing to the clients.",
)
define_metric(
    __monitor__,
    "nginx.connections.waiting",
    "The number of connections currently idle/sending keep alives.",
)


# Taken from:
#   http://stackoverflow.com/questions/1150332/source-interface-with-python-and-urllib2
#
# For connecting to local machine, specifying the source IP may be required.  So, using
# this mechanism should allow that.  Since getting status requires "opening up" a
# non-standard/user-facing web page, it is best to be cautious.
#
# Note - the use of a global is ugly, but this form is more compatible than with another
# method mentioned which would not require the global.  (The cleaner version was added
# in Python 2.7.)
class BindableHTTPConnection(six.moves.http_client.HTTPConnection):
    def connect(self):
        """Connect to the host and port specified in __init__."""
        self.sock = socket.socket()
        self.sock.bind((self.source_ip, 0))
        if isinstance(self.timeout, float):
            self.sock.settimeout(self.timeout)
        self.sock.connect((self.host, self.port))


def BindableHTTPConnectionFactory(source_ip):
    def _get(host, port=None, strict=None, timeout=0):
        # pylint: disable=unexpected-keyword-arg
        # NOTE: "strict" argument is not supported by Python 3 class
        # TODO: We should switch to using urllib Request object directly or requests library
        if six.PY2:
            kwargs = {"strict": strict}
        else:
            kwargs = {}
        bhc = BindableHTTPConnection(host, port=port, timeout=timeout, **kwargs)
        bhc.source_ip = source_ip
        return bhc

    return _get


class BindableHTTPHandler(six.moves.urllib.request.HTTPHandler):
    def http_open(self, req):
        return self.do_open(BindableHTTPConnectionFactory(httpSourceAddress), req)


class NginxMonitor(ScalyrMonitor):
    # fmt: off
    """
# Nginx Monitor

This agent monitor plugin records performance and usage data from an nginx server.

@class=bg-warning docInfoPanel: An *agent monitor plugin* is a component of the Scalyr Agent. To use a plugin,
simply add it to the ``monitors`` section of the Scalyr Agent configuration file (``/etc/scalyr/agent.json``).
For more information, see [Agent Plugins](/help/scalyr-agent#plugins).

## Configuring Nginx

To use this monitor, you will need to configure your nginx server to enable the status module. For details,
see the [nginx documentation](http://nginx.org/en/docs/http/ngx_http_stub_status_module.html).

First, verify that your nginx server supports the status module. Execute the following command:

    nginx -V 2>&1 | grep -o with-http_stub_status_module

The output ``with-http_stub_status_module`` indicates that your server supports the status module. Otherwise,
you will need to either recompile nginx with the ``--with-http_stub_status_module`` flag, or upgrade to a full
version of nginx that has been compiled with that flag.

Next, you must enable the status module, by updating the ``server`` configuration section of your nginx server.
This section can either be found in the ``nginx.conf`` file, or the file in your ``sites-available`` directory
that corresponds to your site. For most Linux systems, these are located at ``/etc/nginx/nginx.conf`` and
``/etc/nginx/sites-available``.

Add the following configuration inside the ``server { ... }`` stanza:

    location /nginx_status {
      stub_status on;      # enable the status module
      allow 127.0.0.1;     # allow connections from localhost only
      deny all;            # deny every other connection
    }

This specifies that the status page should be served at ``http://<address>/nginx_status``, and can't be accessed
from other servers.

Each time the Scalyr agent fetches ``/nginx_status``, an entry will be added to the Nginx access log. If you wish to
prevent this, add the line ``access_log off;`` to the above configuration.

Once you make the configuration change, you will need to restart Nginx.  On most Linux systems, use the following
command:

    sudo service nginx restart

To verify that the status module is working properly, you can view it manually. Execute this command on the server
(substituting the appropriate port number as needed):

    curl http://localhost:80/nginx_status

If you have any difficulty enabling the status module, drop us a line at [support@scalyr.com](mailto:support@scalyr.com).

## Sample Configuration

Here is a typical configuration fragment:

    monitors: [
      {
          module: "scalyr_agent.builtin_monitors.nginx_monitor",
      }
    ]

If you were running an instances of Nginx on a non-standard port (say 8080), your config might resemble:

    monitors: [
      {
          module: "scalyr_agent.builtin_monitors.nginx_monitor",
          status_url: "http://localhost:8080/nginx_status"
          id: "customers"
      }
    ]

Note the ``id`` field in the configurations.  This is an optional field that allows you to specify an identifier
specific to a particular instance of Nginx and will make it easier to filter on metrics specific to that
instance.

accessLog:
## Uploading the nginx access log

If you have not already done so, you should also configure the Scalyr Agent to upload the access log
generated by nginx. Scalyr's nginx dashboard uses this log to generate many statistics.

For most Linux systems, the access log is saved in ``/var/log/nginx/access.log``. To upload, edit the
``logs`` section of ``/etc/scalyr-agent-2/agent.json``. Add the following entry:

    logs: [
       ...

    ***   {***
    ***     path: "/var/log/nginx/access.log",***
    ***     attributes: {parser: "accessLog", serverType: "nginx"}***
    ***   }***
    ]

Edit the ``path`` field as appropriate for your system setup.


## Viewing Data

After adding this plugin to the agent configuration file, wait one minute for data to begin recording. Then
click the {{menuRef:Dashboards}} menu and select {{menuRef:Nginx}}. (The dashboard may not be listed until
the agent begins sending nginx data.) You will see an overview of nginx data across all servers where you are
running the nginx plugin. Use the {{menuRef:ServerHost}} dropdown to show data for a specific server.

See [Analyze Access Logs](/solutions/analyze-access-logs) for more information about working with web access logs.

    """
    # fmt: on

    def _initialize(self):
        global httpSourceAddress
        self.__url = self._config.get("status_url")
        self.__sourceaddress = self._config.get("source_address")
        httpSourceAddress = self.__sourceaddress

    def _parse_data(self, data):
        result = {}
        lines = data.splitlines()
        i = 0
        # skip any blank lines
        while len(lines[i]) == 0:
            i = i + 1

        while i < len(lines):
            if lines[i].startswith(b"Active connections:"):
                result["active_connections"] = int(
                    lines[i][len(b"Active connections: ") :]
                )
            elif lines[i].startswith(b"server accepts handled requests"):
                i = i + 1
                values = lines[i].split()
                result["server_accepts"] = values[0]
                result["server_handled"] = values[1]
                result["server_requests"] = values[2]
            elif lines[i].startswith(b"Reading:"):
                values = lines[i].split()
                result["reading"] = values[1]
                result["writing"] = values[3]
                result["waiting"] = values[5]
            i = i + 1
        return result

    def _get_status(self):
        data = None
        # verify that the URL is valid
        try:
            url = six.moves.urllib.parse.urlparse(self.__url)
        except Exception:
            self._logger.error(
                "The URL configured for requesting the status page appears to be invalid.  Please verify that the URL is correct in your monitor configuration.  The specified url: %s"
                % self.__url
            )
            return data
        # attempt to request server status
        try:
            opener = six.moves.urllib.request.build_opener(BindableHTTPHandler)
            handle = opener.open(self.__url)
            data = handle.read()
            if data is not None:
                data = self._parse_data(data)
        except six.moves.urllib.error.HTTPError as err:
            message = (
                "An HTTP error occurred attempting to retrieve the status.  Please consult your server logs to determine the cause.  HTTP error code: ",
                err.code,
            )
            if err.code == 404:
                message = "The URL used to request the status page appears to be incorrect.  Please verify the correct URL and update your nginx_monitor configuration."
            elif err.code == 403:
                message = "The server is denying access to the URL specified for requesting the status page.  Please verify that permissions to access the status page are correctly configured in your server configuration and that your nginx_monitor configuration reflects the same configuration requirements."
            elif err.code >= 500 or err.code < 600:
                message = (
                    "The server failed to fulfill the request to get the status page.  Please consult your server logs to determine the cause.  HTTP error code: ",
                    err.code,
                )
            self._logger.error(message)
            data = None
        except six.moves.urllib.error.URLError as err:
            message = (
                "The was an error attempting to reach the server.  Make sure the server is running and properly configured.  The error reported is: %s"
                % (str(err))
            )
            if err.reason.errno == 111:
                message = (
                    "The HTTP server does not appear to running or cannot be reached.  Please check that it is running and is reachable at the address: %s"
                    % url.netloc
                )
            self._logger.error(message)
            data = None
        except Exception as e:
            self._logger.error(
                "An error occurred attempting to request the server status: %s" % e
            )
            data = None
        return data

    """
    # Currently disabled as it requires platform specific functionality.  This will need
    # be reactivated once a cross platform solution is implemented.
    def _get_procinfo(self):
        try:
            data = subprocess.Popen("ps aux | grep \"nginx: worker\" | grep -v grep | awk '{print $2, $3, $4}'", shell=True, stdout=subprocess.PIPE).stdout.read()
            result = {}
            lines = data.splitlines()
            i = 0
            while i < len(lines):
                if len(lines[i]) != 0:
                    values = lines[i].split()
                    if len(values) == 3:
                        result[values[0]] = {
                            "cpu": values[1],
                            "mem": values[2]
                        }
                i = i + 1
        except Exception, e:
            self._logger.error("Unable to check process status: %s" % e)
            result = None
        return result
    """

    def gather_sample(self):
        data = self._get_status()
        if data is None:
            self._logger.error("No data returned.")
        else:
            samplesToEmit = [
                ("active_connections", "nginx.connections.active"),
                ("reading", "nginx.connections.reading"),
                ("writing", "nginx.connections.writing"),
                ("waiting", "nginx.connections.waiting"),
            ]

            for key, metric_name in samplesToEmit:
                if key in data:
                    self._logger.emit_value(metric_name, int(data[key]))
