From 8ee4def2b35f54cd428f556c453e933dce086b2e Mon Sep 17 00:00:00 2001 From: chen-li Date: Mon, 22 Aug 2016 22:24:26 -0500 Subject: [PATCH] Add class 'StatsMixin' 1. Add class 'StatsMixin' 2. Fix the listener's stats Change-Id: I7930d52c083c7089382583d657a77ac9969b46ff Implements: blueprint stats-support --- octavia/api/v1/controllers/base.py | 1 + .../api/v1/controllers/listener_statistics.py | 29 ++---- octavia/common/data_models.py | 20 ++++ octavia/common/stats.py | 50 ++++++++++ octavia/controller/healthmanager/update_db.py | 9 +- octavia/tests/unit/common/test_stats.py | 95 +++++++++++++++++++ .../healthmanager/test_update_db.py | 47 +++++---- 7 files changed, 206 insertions(+), 45 deletions(-) create mode 100644 octavia/common/stats.py create mode 100644 octavia/tests/unit/common/test_stats.py diff --git a/octavia/api/v1/controllers/base.py b/octavia/api/v1/controllers/base.py index 15a4eb8537..0668d2d2dc 100644 --- a/octavia/api/v1/controllers/base.py +++ b/octavia/api/v1/controllers/base.py @@ -30,6 +30,7 @@ LOG = logging.getLogger(__name__) class BaseController(rest.RestController): def __init__(self): + super(BaseController, self).__init__() self.repositories = repositories.Repositories() self.handler = stevedore_driver.DriverManager( namespace='octavia.api.handlers', diff --git a/octavia/api/v1/controllers/listener_statistics.py b/octavia/api/v1/controllers/listener_statistics.py index 6263dadd18..3c3c01bdf2 100644 --- a/octavia/api/v1/controllers/listener_statistics.py +++ b/octavia/api/v1/controllers/listener_statistics.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import logging - import pecan from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -21,32 +19,16 @@ from wsmeext import pecan as wsme_pecan from octavia.api.v1.controllers import base from octavia.api.v1.types import listener_statistics as ls_types from octavia.common import constants -from octavia.common import data_models -from octavia.common import exceptions -from octavia.i18n import _LI +from octavia.common import stats -LOG = logging.getLogger(__name__) - - -class ListenerStatisticsController(base.BaseController): +class ListenerStatisticsController(base.BaseController, + stats.StatsMixin): def __init__(self, listener_id): super(ListenerStatisticsController, self).__init__() self.listener_id = listener_id - def _get_db_ls(self, session): - """Gets the current listener statistics object from the database.""" - db_ls = self.repositories.listener_stats.get( - session, listener_id=self.listener_id) - if not db_ls: - LOG.info(_LI("Listener Statistics for Listener %s was not found"), - self.listener_id) - raise exceptions.NotFound( - resource=data_models.ListenerStatistics._name(), - id=self.listener_id) - return db_ls - @wsme_pecan.wsexpose({wtypes.text: ls_types.ListenerStatisticsResponse}) def get_all(self): """Gets a single listener's statistics details.""" @@ -54,6 +36,7 @@ class ListenerStatisticsController(base.BaseController): # listener statistics we are using the get_all method to only get # the single set of stats context = pecan.request.context.get('octavia_context') - db_ls = self._get_db_ls(context.session) + data_stats = self.get_listener_stats( + context.session, self.listener_id) return {constants.LISTENER: self._convert_db_to_type( - db_ls, ls_types.ListenerStatisticsResponse)} + data_stats, ls_types.ListenerStatisticsResponse)} diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 7df214c798..ad79f8ee14 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -146,6 +146,26 @@ class ListenerStatistics(BaseDataModel): self.total_connections = total_connections self.request_errors = request_errors + def get_stats(self): + stats = { + 'bytes_in': self.bytes_in, + 'bytes_out': self.bytes_out, + 'active_connections': self.active_connections, + 'total_connections': self.total_connections, + 'request_errors': self.request_errors, + } + return stats + + def __iadd__(self, other): + + if isinstance(other, ListenerStatistics): + self.bytes_in += other.bytes_in + self.bytes_out += other.bytes_out + self.request_errors += other.request_errors + self.total_connections += other.total_connections + + return self + class HealthMonitor(BaseDataModel): diff --git a/octavia/common/stats.py b/octavia/common/stats.py new file mode 100644 index 0000000000..397b254699 --- /dev/null +++ b/octavia/common/stats.py @@ -0,0 +1,50 @@ +# Copyright 2016 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from octavia.common import constants +from octavia.common import data_models +from octavia.db import repositories as repo +from octavia.i18n import _LW + + +LOG = logging.getLogger(__name__) + + +class StatsMixin(object): + + def __init__(self): + super(StatsMixin, self).__init__() + self.listener_stats_repo = repo.ListenerStatisticsRepository() + self.repo_amphora = repo.AmphoraRepository() + + def get_listener_stats(self, session, listener_id): + """Gets the listener statistics data_models object.""" + db_ls = self.listener_stats_repo.get_all( + session, listener_id=listener_id) + if not db_ls: + LOG.warning( + _LW("Listener Statistics for Listener %s was not found"), + listener_id) + + statistics = data_models.ListenerStatistics(listener_id=listener_id) + + for db_l in db_ls: + statistics += db_l + + amp = self.repo_amphora.get(session, id=db_l.amphora_id) + if amp and amp.status == constants.AMPHORA_ALLOCATED: + statistics.active_connections += db_l.active_connections + return statistics diff --git a/octavia/controller/healthmanager/update_db.py b/octavia/controller/healthmanager/update_db.py index b320d0bb72..f41d834e5a 100644 --- a/octavia/controller/healthmanager/update_db.py +++ b/octavia/controller/healthmanager/update_db.py @@ -21,6 +21,7 @@ import sqlalchemy from stevedore import driver as stevedore_driver from octavia.common import constants +from octavia.common import stats from octavia.controller.healthmanager import update_serializer from octavia.controller.queue import event_queue from octavia.db import api as db_api @@ -211,11 +212,10 @@ class UpdateHealthDb(object): LOG.error(_LE("Load balancer %s is not in DB"), lb_id) -class UpdateStatsDb(object): +class UpdateStatsDb(stats.StatsMixin): def __init__(self): super(UpdateStatsDb, self).__init__() - self.listener_stats_repo = repo.ListenerStatisticsRepository() self.event_streamer = event_queue.EventStreamerNeutron() def emit(self, info_type, info_id, info_obj): @@ -268,4 +268,7 @@ class UpdateStatsDb(object): listener_id, amphora_id, stats) self.listener_stats_repo.replace( session, listener_id, amphora_id, **stats) - self.emit('listener_stats', listener_id, stats) + + listener_stats = self.get_listener_stats(session, listener_id) + self.emit( + 'listener_stats', listener_id, listener_stats.get_stats()) diff --git a/octavia/tests/unit/common/test_stats.py b/octavia/tests/unit/common/test_stats.py new file mode 100644 index 0000000000..b3be80d461 --- /dev/null +++ b/octavia/tests/unit/common/test_stats.py @@ -0,0 +1,95 @@ +# Copyright 2016 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random + +import mock +from oslo_utils import uuidutils + +from octavia.common import constants +from octavia.common import data_models +from octavia.common import stats +from octavia.tests.unit import base + + +class TestStatsMixin(base.TestCase): + + def setUp(self): + super(TestStatsMixin, self).setUp() + self.sm = stats.StatsMixin() + + self.session = mock.MagicMock() + self.listener_id = uuidutils.generate_uuid() + self.amphora_id = uuidutils.generate_uuid() + + self.repo_listener_stats = mock.MagicMock() + self.sm.listener_stats_repo = self.repo_listener_stats + + self.fake_stats = data_models.ListenerStatistics( + listener_id=self.listener_id, + amphora_id=self.amphora_id, + bytes_in=random.randrange(1000000000), + bytes_out=random.randrange(1000000000), + active_connections=random.randrange(1000000000), + total_connections=random.randrange(1000000000), + request_errors=random.randrange(1000000000)) + + self.sm.listener_stats_repo.get_all.return_value = [self.fake_stats] + + self.repo_amphora = mock.MagicMock() + self.sm.repo_amphora = self.repo_amphora + + def test_get_listener_stats(self): + fake_amp = mock.MagicMock() + fake_amp.status = constants.AMPHORA_ALLOCATED + self.sm.repo_amphora.get.return_value = fake_amp + + ls_stats = self.sm.get_listener_stats( + self.session, self.listener_id) + self.repo_listener_stats.get_all.assert_called_once_with( + self.session, listener_id=self.listener_id) + self.repo_amphora.get.assert_called_once_with( + self.session, id=self.amphora_id) + + self.assertEqual(self.fake_stats.bytes_in, ls_stats.bytes_in) + self.assertEqual(self.fake_stats.bytes_out, ls_stats.bytes_out) + self.assertEqual( + self.fake_stats.active_connections, ls_stats.active_connections) + self.assertEqual( + self.fake_stats.total_connections, ls_stats.total_connections) + self.assertEqual( + self.fake_stats.request_errors, ls_stats.request_errors) + self.assertEqual(self.listener_id, ls_stats.listener_id) + self.assertIsNone(ls_stats.amphora_id) + + def test_get_listener_stats_with_amphora_deleted(self): + fake_amp = mock.MagicMock() + fake_amp.status = constants.DELETED + self.sm.repo_amphora.get.return_value = fake_amp + + ls_stats = self.sm.get_listener_stats(self.session, self.listener_id) + self.repo_listener_stats.get_all.assert_called_once_with( + self.session, listener_id=self.listener_id) + self.repo_amphora.get.assert_called_once_with( + self.session, id=self.amphora_id) + + self.assertEqual(self.fake_stats.bytes_in, ls_stats.bytes_in) + self.assertEqual(self.fake_stats.bytes_out, ls_stats.bytes_out) + self.assertEqual(0, ls_stats.active_connections) + self.assertEqual( + self.fake_stats.total_connections, ls_stats.total_connections) + self.assertEqual( + self.fake_stats.request_errors, ls_stats.request_errors) + self.assertEqual(self.listener_id, ls_stats.listener_id) + self.assertIsNone(ls_stats.amphora_id) diff --git a/octavia/tests/unit/controller/healthmanager/test_update_db.py b/octavia/tests/unit/controller/healthmanager/test_update_db.py index ec3a9c599f..779327a792 100644 --- a/octavia/tests/unit/controller/healthmanager/test_update_db.py +++ b/octavia/tests/unit/controller/healthmanager/test_update_db.py @@ -457,14 +457,20 @@ class TestUpdateStatsDb(base.TestCase): self.listener_stats_repo = mock.MagicMock() self.sm.listener_stats_repo = self.listener_stats_repo - self.bytes_in = random.randrange(1000000000) - self.bytes_out = random.randrange(1000000000) - self.request_errors = random.randrange(1000000000) - self.active_conns = random.randrange(1000000000) - self.total_conns = random.randrange(1000000000) self.loadbalancer_id = uuidutils.generate_uuid() self.listener_id = uuidutils.generate_uuid() + self.listener_stats = data_models.ListenerStatistics( + listener_id=self.listener_id, + bytes_in=random.randrange(1000000000), + bytes_out=random.randrange(1000000000), + active_connections=random.randrange(1000000000), + total_connections=random.randrange(1000000000), + request_errors=random.randrange(1000000000)) + + self.sm.get_listener_stats = mock.MagicMock( + return_value=self.listener_stats) + @mock.patch('octavia.db.api.get_session') def test_update_stats(self, session): @@ -474,11 +480,11 @@ class TestUpdateStatsDb(base.TestCase): self.listener_id: { "status": constants.OPEN, "stats": { - "ereq": self.request_errors, - "conns": self.active_conns, - "totconns": self.total_conns, - "rx": self.bytes_in, - "tx": self.bytes_out, + "ereq": self.listener_stats.request_errors, + "conns": self.listener_stats.active_connections, + "totconns": self.listener_stats.total_connections, + "rx": self.listener_stats.bytes_in, + "tx": self.listener_stats.bytes_out, }, "pools": { "pool-id-1": { @@ -496,17 +502,20 @@ class TestUpdateStatsDb(base.TestCase): self.listener_stats_repo.replace.assert_called_once_with( 'blah', self.listener_id, self.loadbalancer_id, - bytes_in=self.bytes_in, bytes_out=self.bytes_out, - active_connections=self.active_conns, - total_connections=self.total_conns, - request_errors=self.request_errors) + bytes_in=self.listener_stats.bytes_in, + bytes_out=self.listener_stats.bytes_out, + active_connections=self.listener_stats.active_connections, + total_connections=self.listener_stats.total_connections, + request_errors=self.listener_stats.request_errors) self.event_client.cast.assert_called_once_with( {}, 'update_info', container={ 'info_type': 'listener_stats', 'info_id': self.listener_id, 'info_payload': { - 'bytes_in': self.bytes_in, - 'total_connections': self.total_conns, - 'active_connections': self.active_conns, - 'bytes_out': self.bytes_out, - 'request_errors': self.request_errors}}) + 'bytes_in': self.listener_stats.bytes_in, + 'total_connections': + self.listener_stats.total_connections, + 'active_connections': + self.listener_stats.active_connections, + 'bytes_out': self.listener_stats.bytes_out, + 'request_errors': self.listener_stats.request_errors}})