Add a new API to list loadbalancer statistics
Change-Id: I4521fdaa2f1e2f8f80d5def311ae67937e84479a Implements: blueprint stats-support
This commit is contained in:
parent
8ee4def2b3
commit
e6507ed080
@ -168,6 +168,42 @@ Retrieve details of a load balancer.
|
||||
}
|
||||
|
||||
|
||||
List Load Balancer Statistics
|
||||
*****************************
|
||||
|
||||
Retrieve the stats of a load balancer.
|
||||
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| Request Type | ``GET`` |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| Endpoint | ``URL/v1/loadbalancers/{lb_id}/stats`` |
|
||||
+----------------+---------+-------------------------------------------------+
|
||||
| | Success | 200 |
|
||||
| Response Codes +---------+-------------------------------------------------+
|
||||
| | Error | 401, 404, 500 |
|
||||
+----------------+---------+-------------------------------------------------+
|
||||
|
||||
**Response Example**::
|
||||
|
||||
{
|
||||
"loadbalancer": {
|
||||
"bytes_in": 0,
|
||||
"bytes_out": 0,
|
||||
"active_connections": 0,
|
||||
"total_connections": 0,
|
||||
"request_errors": 0,
|
||||
"listeners": [{
|
||||
"id": "uuid"
|
||||
"bytes_in": 0,
|
||||
"bytes_out": 0,
|
||||
"active_connections": 0,
|
||||
"total_connections": 0,
|
||||
"request_errors": 0,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Create Load Balancer
|
||||
********************
|
||||
|
||||
|
@ -22,6 +22,7 @@ from wsmeext import pecan as wsme_pecan
|
||||
|
||||
from octavia.api.v1.controllers import base
|
||||
from octavia.api.v1.controllers import listener
|
||||
from octavia.api.v1.controllers import load_balancer_statistics as lb_stats
|
||||
from octavia.api.v1.controllers import pool
|
||||
from octavia.api.v1.types import load_balancer as lb_types
|
||||
from octavia.common import constants
|
||||
@ -178,9 +179,9 @@ class LoadBalancersController(base.BaseController):
|
||||
decides which controller, if any, should control be passed.
|
||||
"""
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
if lb_id and len(remainder) and (remainder[0] == 'listeners' or
|
||||
remainder[0] == 'pools' or
|
||||
remainder[0] == 'delete_cascade'):
|
||||
|
||||
possible_remainder = ('listeners', 'pools', 'delete_cascade', 'stats')
|
||||
if lb_id and len(remainder) and (remainder[0] in possible_remainder):
|
||||
controller = remainder[0]
|
||||
remainder = remainder[1:]
|
||||
db_lb = self.repositories.load_balancer.get(context.session,
|
||||
@ -197,6 +198,9 @@ class LoadBalancersController(base.BaseController):
|
||||
load_balancer_id=db_lb.id), remainder
|
||||
elif (controller == 'delete_cascade'):
|
||||
return LBCascadeDeleteController(db_lb.id), ''
|
||||
elif (controller == 'stats'):
|
||||
return lb_stats.LoadBalancerStatisticsController(
|
||||
loadbalancer_id=db_lb.id), remainder
|
||||
|
||||
|
||||
class LBCascadeDeleteController(LoadBalancersController):
|
||||
|
40
octavia/api/v1/controllers/load_balancer_statistics.py
Normal file
40
octavia/api/v1/controllers/load_balancer_statistics.py
Normal file
@ -0,0 +1,40 @@
|
||||
# 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 pecan
|
||||
from wsme import types as wtypes
|
||||
from wsmeext import pecan as wsme_pecan
|
||||
|
||||
from octavia.api.v1.controllers import base
|
||||
from octavia.api.v1.types import load_balancer_statistics as lb_types
|
||||
from octavia.common import constants
|
||||
from octavia.common import stats
|
||||
|
||||
|
||||
class LoadBalancerStatisticsController(base.BaseController,
|
||||
stats.StatsMixin):
|
||||
|
||||
def __init__(self, loadbalancer_id):
|
||||
super(LoadBalancerStatisticsController, self).__init__()
|
||||
self.loadbalancer_id = loadbalancer_id
|
||||
|
||||
@wsme_pecan.wsexpose(
|
||||
{wtypes.text: lb_types.LoadBalancerStatisticsResponse})
|
||||
def get(self):
|
||||
"""Gets a single loadbalancer's statistics details."""
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
data_stats = self.get_loadbalancer_stats(
|
||||
context.session, self.loadbalancer_id)
|
||||
return {constants.LOADBALANCER: self._convert_db_to_type(
|
||||
data_stats, lb_types.LoadBalancerStatisticsResponse)}
|
50
octavia/api/v1/types/load_balancer_statistics.py
Normal file
50
octavia/api/v1/types/load_balancer_statistics.py
Normal file
@ -0,0 +1,50 @@
|
||||
# Copyright 2016 Blue Box, an IBM Company
|
||||
#
|
||||
# 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.
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from octavia.api.v1.types import base
|
||||
from octavia.api.v1.types import listener_statistics
|
||||
|
||||
|
||||
class ListenerStatistics(listener_statistics.ListenerStatisticsResponse):
|
||||
id = wtypes.wsattr(wtypes.UuidType())
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
ls_stats = super(ListenerStatistics, cls).from_data_model(
|
||||
data_model, children=children)
|
||||
ls_stats.id = data_model.listener_id
|
||||
return ls_stats
|
||||
|
||||
|
||||
class LoadBalancerStatisticsResponse(base.BaseType):
|
||||
bytes_in = wtypes.wsattr(wtypes.IntegerType())
|
||||
bytes_out = wtypes.wsattr(wtypes.IntegerType())
|
||||
active_connections = wtypes.wsattr(wtypes.IntegerType())
|
||||
total_connections = wtypes.wsattr(wtypes.IntegerType())
|
||||
request_errors = wtypes.wsattr(wtypes.IntegerType())
|
||||
listeners = wtypes.wsattr([ListenerStatistics])
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
lb_stats = super(LoadBalancerStatisticsResponse, cls).from_data_model(
|
||||
data_model, children=children)
|
||||
|
||||
lb_stats.listeners = [
|
||||
ListenerStatistics.from_data_model(
|
||||
listener_dm, children=children)
|
||||
for listener_dm in data_model.listeners
|
||||
]
|
||||
return lb_stats
|
@ -167,6 +167,28 @@ class ListenerStatistics(BaseDataModel):
|
||||
return self
|
||||
|
||||
|
||||
class LoadBalancerStatistics(BaseDataModel):
|
||||
|
||||
def __init__(self, bytes_in=0, bytes_out=0, active_connections=0,
|
||||
total_connections=0, request_errors=0, listeners=None):
|
||||
self.bytes_in = bytes_in
|
||||
self.bytes_out = bytes_out
|
||||
self.active_connections = active_connections
|
||||
self.total_connections = total_connections
|
||||
self.request_errors = request_errors
|
||||
self.listeners = listeners or []
|
||||
|
||||
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
|
||||
|
||||
|
||||
class HealthMonitor(BaseDataModel):
|
||||
|
||||
def __init__(self, id=None, project_id=None, pool_id=None, type=None,
|
||||
|
@ -29,6 +29,7 @@ class StatsMixin(object):
|
||||
super(StatsMixin, self).__init__()
|
||||
self.listener_stats_repo = repo.ListenerStatisticsRepository()
|
||||
self.repo_amphora = repo.AmphoraRepository()
|
||||
self.repo_loadbalancer = repo.LoadBalancerRepository()
|
||||
|
||||
def get_listener_stats(self, session, listener_id):
|
||||
"""Gets the listener statistics data_models object."""
|
||||
@ -48,3 +49,17 @@ class StatsMixin(object):
|
||||
if amp and amp.status == constants.AMPHORA_ALLOCATED:
|
||||
statistics.active_connections += db_l.active_connections
|
||||
return statistics
|
||||
|
||||
def get_loadbalancer_stats(self, session, loadbalancer_id):
|
||||
statistics = data_models.LoadBalancerStatistics()
|
||||
lb_db = self.repo_loadbalancer.get(session, id=loadbalancer_id)
|
||||
|
||||
for listener in lb_db.listeners:
|
||||
data = self.get_listener_stats(session, listener.id)
|
||||
statistics.bytes_in += data.bytes_in
|
||||
statistics.bytes_out += data.bytes_out
|
||||
statistics.request_errors += data.request_errors
|
||||
statistics.active_connections += data.active_connections
|
||||
statistics.total_connections += data.total_connections
|
||||
statistics.listeners.append(data)
|
||||
return statistics
|
||||
|
@ -217,6 +217,7 @@ class UpdateStatsDb(stats.StatsMixin):
|
||||
def __init__(self):
|
||||
super(UpdateStatsDb, self).__init__()
|
||||
self.event_streamer = event_queue.EventStreamerNeutron()
|
||||
self.repo_listener = repo.ListenerRepository()
|
||||
|
||||
def emit(self, info_type, info_id, info_obj):
|
||||
cnt = update_serializer.InfoContainer(info_type, info_id, info_obj)
|
||||
@ -272,3 +273,9 @@ class UpdateStatsDb(stats.StatsMixin):
|
||||
listener_stats = self.get_listener_stats(session, listener_id)
|
||||
self.emit(
|
||||
'listener_stats', listener_id, listener_stats.get_stats())
|
||||
|
||||
listener_db = self.repo_listener.get(session, id=listener_id)
|
||||
lb_stats = self.get_loadbalancer_stats(
|
||||
session, listener_db.load_balancer_id)
|
||||
self.emit('loadbalancer_stats',
|
||||
listener_db.load_balancer_id, lb_stats.get_stats())
|
||||
|
@ -31,6 +31,7 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
|
||||
BASE_PATH = '/v1'
|
||||
LBS_PATH = '/loadbalancers'
|
||||
LB_PATH = LBS_PATH + '/{lb_id}'
|
||||
LB_STATS_PATH = LB_PATH + '/stats'
|
||||
LISTENERS_PATH = LB_PATH + '/listeners'
|
||||
LISTENER_PATH = LISTENERS_PATH + '/{listener_id}'
|
||||
LISTENER_STATS_PATH = LISTENER_PATH + '/stats'
|
||||
|
@ -0,0 +1,56 @@
|
||||
# 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.
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.tests.functional.api.v1 import base
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
|
||||
class TestLoadBlancerStatistics(base.BaseAPITest):
|
||||
FAKE_UUID_1 = uuidutils.generate_uuid()
|
||||
|
||||
def setUp(self):
|
||||
super(TestLoadBlancerStatistics, self).setUp()
|
||||
self.lb = self.create_load_balancer({})
|
||||
self.set_lb_status(self.lb.get('id'))
|
||||
self.listener = self.create_listener(self.lb.get('id'),
|
||||
constants.PROTOCOL_HTTP, 80)
|
||||
self.set_lb_status(self.lb.get('id'))
|
||||
self.lb_path = self.LB_STATS_PATH.format(lb_id=self.lb.get('id'))
|
||||
self.amphora = self.create_amphora(uuidutils.generate_uuid(),
|
||||
self.lb.get('id'))
|
||||
|
||||
def test_get(self):
|
||||
ls = self.create_listener_stats(listener_id=self.listener.get('id'),
|
||||
amphora_id=self.amphora.id)
|
||||
expected = {
|
||||
'loadbalancer': {
|
||||
'bytes_in': ls['bytes_in'],
|
||||
'bytes_out': ls['bytes_out'],
|
||||
'active_connections': ls['active_connections'],
|
||||
'total_connections': ls['total_connections'],
|
||||
'request_errors': ls['request_errors'],
|
||||
'listeners': [
|
||||
{'id': self.listener.get('id'),
|
||||
'bytes_in': ls['bytes_in'],
|
||||
'bytes_out': ls['bytes_out'],
|
||||
'active_connections': ls['active_connections'],
|
||||
'total_connections': ls['total_connections'],
|
||||
'request_errors': ls['request_errors']}]
|
||||
}
|
||||
}
|
||||
response = self.get(self.lb_path)
|
||||
response_body = response.json
|
||||
self.assertEqual(expected, response_body)
|
@ -24,6 +24,7 @@ import sqlalchemy
|
||||
from octavia.common import constants
|
||||
from octavia.common import data_models
|
||||
from octavia.controller.healthmanager import update_db
|
||||
from octavia.db import models as db_models
|
||||
from octavia.tests.unit import base
|
||||
|
||||
|
||||
@ -468,14 +469,33 @@ class TestUpdateStatsDb(base.TestCase):
|
||||
total_connections=random.randrange(1000000000),
|
||||
request_errors=random.randrange(1000000000))
|
||||
|
||||
self.sm.get_listener_stats = mock.MagicMock(
|
||||
return_value=self.listener_stats)
|
||||
self.sm.get_listener_stats = mock.MagicMock()
|
||||
self.sm.get_listener_stats.return_value = self.listener_stats
|
||||
|
||||
self.loadbalancer_id = uuidutils.generate_uuid()
|
||||
self.amphora_id = uuidutils.generate_uuid()
|
||||
self.listener_id = uuidutils.generate_uuid()
|
||||
|
||||
self.listener = db_models.Listener(
|
||||
load_balancer_id=self.loadbalancer_id)
|
||||
|
||||
self.listener_repo = mock.MagicMock()
|
||||
self.sm.repo_listener = self.listener_repo
|
||||
self.sm.repo_listener.get.return_value = self.listener
|
||||
|
||||
self.loadbalancer_repo = mock.MagicMock()
|
||||
self.sm.repo_loadbalancer = self.loadbalancer_repo
|
||||
|
||||
self.loadbalancer = db_models.LoadBalancer(
|
||||
id=self.loadbalancer_id,
|
||||
listeners=[self.listener])
|
||||
self.loadbalancer_repo.get.return_value = self.loadbalancer
|
||||
|
||||
@mock.patch('octavia.db.api.get_session')
|
||||
def test_update_stats(self, session):
|
||||
|
||||
health = {
|
||||
"id": self.loadbalancer_id,
|
||||
"id": self.amphora_id,
|
||||
"listeners": {
|
||||
self.listener_id: {
|
||||
"status": constants.OPEN,
|
||||
@ -501,13 +521,13 @@ class TestUpdateStatsDb(base.TestCase):
|
||||
self.sm.update_stats(health)
|
||||
|
||||
self.listener_stats_repo.replace.assert_called_once_with(
|
||||
'blah', self.listener_id, self.loadbalancer_id,
|
||||
'blah', self.listener_id, self.amphora_id,
|
||||
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(
|
||||
self.event_client.cast.assert_any_call(
|
||||
{}, 'update_info', container={
|
||||
'info_type': 'listener_stats',
|
||||
'info_id': self.listener_id,
|
||||
@ -519,3 +539,17 @@ class TestUpdateStatsDb(base.TestCase):
|
||||
self.listener_stats.active_connections,
|
||||
'bytes_out': self.listener_stats.bytes_out,
|
||||
'request_errors': self.listener_stats.request_errors}})
|
||||
|
||||
self.event_client.cast.assert_any_call(
|
||||
{}, 'update_info',
|
||||
container={
|
||||
'info_type': 'loadbalancer_stats',
|
||||
'info_id': self.loadbalancer_id,
|
||||
'info_payload': {
|
||||
'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}})
|
||||
|
Loading…
Reference in New Issue
Block a user