* RGW sync perf. counters are now exposed through grafana panels.
* Sync Performance tab is only shown if rgw realm is detected.
* Prometheus module: added metrics suitable for prometheus consumption (from existing ones, not replacing for backward compatibility).
Fixes: https://tracker.ceph.com/issues/45310
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
--- /dev/null
+{
+ "__requires": [
+ {
+ "type": "grafana",
+ "id": "grafana",
+ "name": "Grafana",
+ "version": "5.0.0"
+ },
+ {
+ "type": "panel",
+ "id": "graph",
+ "name": "Graph",
+ "version": "5.0.0"
+ }
+ ],
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": false,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "id": null,
+ "iteration": 1534386107523,
+ "links": [],
+ "panels": [
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "$datasource",
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 8,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (source_zone) (rate(ceph_data_sync_from_zone_fetch_bytes_sum[30s]))",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "{{source_zone}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Replication (throughput) from Source Zone",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "unit": "bytes",
+ "format": "Bps",
+ "decimals": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": false
+ }
+ ]
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "$datasource",
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 7.4,
+ "x": 8.3,
+ "y": 0
+ },
+ "id": 2,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (source_zone) (rate(ceph_data_sync_from_zone_fetch_bytes_count[30s]))",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "{{source_zone}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Replication (objects) from Source Zone",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "decimals": null,
+ "label": "Objects/s",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": false
+ }
+ ]
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "$datasource",
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 8,
+ "x": 16,
+ "y": 0
+ },
+ "id": 3,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (source_zone) (rate(ceph_data_sync_from_zone_poll_latency_sum[30s]) * 1000)",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "{{source_zone}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Polling Request Latency from Source Zone",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "unit": "s",
+ "format": "ms",
+ "decimals": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": false
+ }
+ ]
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "$datasource",
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 8,
+ "x": 0,
+ "y": 7
+ },
+ "id": 4,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (source_zone) (rate(ceph_data_sync_from_zone_fetch_errors[30s]))",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "legendFormat": "{{source_zone}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Unsuccessful Object Replications from Source Zone",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "decimals": null,
+ "label": "Count/s",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": false
+ }
+ ]
+ }
+ ],
+ "refresh": "15s",
+ "schemaVersion": 16,
+ "style": "dark",
+ "tags": [
+ "overview"
+ ],
+ "templating": {
+ "list": [
+ {
+ "allValue": null,
+ "current": {},
+ "datasource": "$datasource",
+ "hide": 2,
+ "includeAll": true,
+ "label": null,
+ "multi": false,
+ "name": "rgw_servers",
+ "options": [],
+ "query": "prometheus",
+ "refresh": 1,
+ "regex": "",
+ "sort": 1,
+ "tagValuesQuery": "",
+ "tags": [],
+ "tagsQuery": "",
+ "type": "query",
+ "useTags": false
+ },
+ {
+ "current": {
+ "tags": [],
+ "text": "default",
+ "value": "default"
+ },
+ "hide": 0,
+ "label": "Data Source",
+ "name": "datasource",
+ "options": [],
+ "query": "prometheus",
+ "refresh": 1,
+ "regex": "",
+ "type": "datasource"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "5s",
+ "10s",
+ "15s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ],
+ "time_options": [
+ "5m",
+ "15m",
+ "1h",
+ "6h",
+ "12h",
+ "24h",
+ "2d",
+ "7d",
+ "30d"
+ ]
+ },
+ "timezone": "",
+ "title": "RGW Sync Overview",
+ "uid": "rgw-sync-overview",
+ "version": 2
+}
data['message'])
+class RgwSiteTest(RgwTestCase):
+
+ AUTH_ROLES = ['rgw-manager']
+
+ def test_get_placement_targets(self):
+ data = self._get('/api/rgw/site?query=placement-targets')
+ self.assertStatus(200)
+ self.assertSchema(data, JObj({
+ 'zonegroup': str,
+ 'placement_targets': JList(JObj({
+ 'name': str,
+ 'data_pool': str
+ }))
+ }))
+
+ def test_get_realms(self):
+ data = self._get('/api/rgw/site?query=realms')
+ self.assertStatus(200)
+ self.assertSchema(data, JList(str))
+
+
class RgwBucketTest(RgwTestCase):
_mfa_token_serial = '1'
class RgwSite(RgwRESTController):
def list(self, query=None):
if query == 'placement-targets':
- instance = RgwClient.admin_instance()
- result = instance.get_placement_targets()
+ result = RgwClient.admin_instance().get_placement_targets()
+ elif query == 'realms':
+ result = RgwClient.admin_instance().get_realms()
else:
- # @TODO: (it'll be required for multisite workflows):
- # by default, retrieve cluster realms/zonegroups map.
+ # @TODO: for multisite: by default, retrieve cluster topology/map.
raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
return result
component = fixture.componentInstance;
rgwBucketService = TestBed.get(RgwBucketService);
rgwBucketServiceGetSpy = spyOn(rgwBucketService, 'get');
- getPlacementTargetsSpy = spyOn(TestBed.get(RgwSiteService), 'getPlacementTargets');
+ getPlacementTargetsSpy = spyOn(TestBed.get(RgwSiteService), 'get');
enumerateSpy = spyOn(TestBed.get(RgwUserService), 'enumerate');
formHelper = new FormHelper(component.bucketForm);
});
};
if (!this.editing) {
- promises['getPlacementTargets'] = this.rgwSiteService.getPlacementTargets();
+ promises['getPlacementTargets'] = this.rgwSiteService.get('placement-targets');
}
// Process route parameters.
grafanaStyle="two">
</cd-grafana>
</tab>
+
+ <tab i18n-heading
+ *ngIf="grafanaPermission.read && isMultiSite"
+ heading="Sync Performance">
+ <cd-grafana [grafanaPath]="'radosgw-sync-overview?'"
+ uid="rgw-sync-overview"
+ grafanaStyle="two">
+ </cd-grafana>
+ </tab>
</tabset>
import { RouterTestingModule } from '@angular/router/testing';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { of } from 'rxjs';
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { RgwSiteService } from '../../../shared/api/rgw-site.service';
+import { Permissions } from '../../../shared/models/permissions';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { SharedModule } from '../../../shared/shared.module';
import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
describe('RgwDaemonListComponent', () => {
let component: RgwDaemonListComponent;
let fixture: ComponentFixture<RgwDaemonListComponent>;
+ let getPermissionsSpy: jasmine.Spy;
+ let getRealmsSpy: jasmine.Spy;
+ const permissions = new Permissions({ grafana: ['read'] });
+ const expectTabsAndHeading = (length: number, heading: string) => {
+ const tabs = fixture.debugElement.nativeElement.querySelectorAll('tab');
+
+ expect(tabs.length).toEqual(length);
+ expect(tabs[length - 1].getAttribute('heading')).toEqual(heading);
+ };
configureTestBed({
declarations: [RgwDaemonListComponent, RgwDaemonDetailsComponent],
});
beforeEach(() => {
+ getPermissionsSpy = spyOn(TestBed.get(AuthStorageService), 'getPermissions');
+ getPermissionsSpy.and.returnValue(new Permissions({}));
+ getRealmsSpy = spyOn(TestBed.get(RgwSiteService), 'get');
+ getRealmsSpy.and.returnValue(of([]));
fixture = TestBed.createComponent(RgwDaemonListComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
});
it('should create', () => {
+ fixture.detectChanges();
expect(component).toBeTruthy();
});
+
+ it('should only show Daemons List tab', () => {
+ fixture.detectChanges();
+
+ expectTabsAndHeading(1, 'Daemons List');
+ });
+
+ it('should show Overall Performance tab', () => {
+ getPermissionsSpy.and.returnValue(permissions);
+ fixture.detectChanges();
+
+ expectTabsAndHeading(2, 'Overall Performance');
+ });
+
+ it('should show Sync Performance tab', () => {
+ getPermissionsSpy.and.returnValue(permissions);
+ getRealmsSpy.and.returnValue(of(['realm1']));
+ fixture.detectChanges();
+
+ expectTabsAndHeading(3, 'Sync Performance');
+ });
});
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
import { RgwDaemonService } from '../../../shared/api/rgw-daemon.service';
+import { RgwSiteService } from '../../../shared/api/rgw-site.service';
import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
templateUrl: './rgw-daemon-list.component.html',
styleUrls: ['./rgw-daemon-list.component.scss']
})
-export class RgwDaemonListComponent extends ListWithDetails {
+export class RgwDaemonListComponent extends ListWithDetails implements OnInit {
columns: CdTableColumn[] = [];
daemons: object[] = [];
grafanaPermission: Permission;
+ isMultiSite: boolean;
constructor(
private rgwDaemonService: RgwDaemonService,
private authStorageService: AuthStorageService,
- cephShortVersionPipe: CephShortVersionPipe,
- private i18n: I18n
+ private cephShortVersionPipe: CephShortVersionPipe,
+ private i18n: I18n,
+ private rgwSiteService: RgwSiteService
) {
super();
+ }
+
+ ngOnInit(): void {
this.grafanaPermission = this.authStorageService.getPermissions().grafana;
this.columns = [
{
name: this.i18n('Version'),
prop: 'version',
flexGrow: 1,
- pipe: cephShortVersionPipe
+ pipe: this.cephShortVersionPipe
}
];
+ this.rgwSiteService
+ .get('realms')
+ .subscribe((realms: string[]) => (this.isMultiSite = realms.length > 0));
}
getDaemonList(context: CdTableFetchDataContext) {
expect(service).toBeTruthy();
});
- it('should call getPlacementTargets', () => {
- service.getPlacementTargets().subscribe();
- const req = httpTesting.expectOne('api/rgw/site?query=placement-targets');
+ it('should contain site endpoint in GET request', () => {
+ service.get().subscribe();
+ const req = httpTesting.expectOne(service['url']);
expect(req.request.method).toBe('GET');
});
+
+ it('should add query param in GET request', () => {
+ const query = 'placement-targets';
+ service.get(query).subscribe();
+ httpTesting.expectOne(`${service['url']}?query=placement-targets`);
+ });
});
constructor(private http: HttpClient) {}
- getPlacementTargets() {
+ get(query?: string) {
let params = new HttpParams();
- params = params.append('query', 'placement-targets');
+ if (query) {
+ params = params.append('query', query);
+ }
return this.http.get(this.url, { params: params });
}
['api_name', 'zones']
) for zonegroup in zonegroups['zonegroups']]
+ def _get_realms_info(self): # type: () -> dict
+ return json_str_to_object(self.proxy('GET', 'realm?list', None, None))
+
@staticmethod
def _rgw_settings():
return (Settings.RGW_API_HOST,
return {'zonegroup': zonegroup_name, 'placement_targets': placement_targets}
+ def get_realms(self): # type: () -> List
+ realms_info = self._get_realms_info()
+ if 'realms' in realms_info and realms_info['realms']:
+ return realms_info['realms']
+
+ return []
+
@RestClient.api_get('/{bucket_name}?versioning')
def get_bucket_versioning(self, bucket_name, request=None):
"""
import unittest
try:
- from mock import patch
-except ImportError:
from unittest.mock import patch
+except ImportError:
+ from mock import patch # type: ignore
from ..services.rgw_client import RgwClient, _parse_frontend_config
from ..settings import Settings
}
self.assertEqual(expected_result, instance.get_placement_targets())
+ @patch.object(RgwClient, '_get_realms_info')
+ def test_get_realms(self, realms_info):
+ realms_info.side_effect = [
+ {
+ 'default_info': '51de8373-bc24-4f74-a9b7-8e9ef4cb71f7',
+ 'realms': [
+ 'realm1',
+ 'realm2'
+ ]
+ },
+ {}
+ ]
+ instance = RgwClient.admin_instance()
+
+ self.assertEqual(['realm1', 'realm2'], instance.get_realms())
+ self.assertEqual([], instance.get_realms())
+
class RgwClientHelperTest(unittest.TestCase):
def test_parse_frontend_config_1(self):
del self.rbd_stats['query']
self.rbd_stats['pools'].clear()
+ def add_fixed_name_metrics(self):
+ """
+ Add fixed name metrics from existing ones that have details in their names
+ that should be in labels (not in name).
+ For backward compatibility, a new fixed name metric is created (instead of replacing)
+ and details are put in new labels.
+ Intended for RGW sync perf. counters but extendable as required.
+ See: https://tracker.ceph.com/issues/45311
+ """
+ new_metrics = {}
+ for metric_path in self.metrics.keys():
+ # Address RGW sync perf. counters.
+ match = re.search('^data-sync-from-(.*)\.', metric_path)
+ if match:
+ new_path = re.sub('from-([^.]*)', 'from-zone', metric_path)
+ if new_path not in new_metrics:
+ new_metrics[new_path] = Metric(
+ self.metrics[metric_path].mtype,
+ new_path,
+ self.metrics[metric_path].desc,
+ self.metrics[metric_path].labelnames + ('source_zone',)
+ )
+ for label_values, value in self.metrics[metric_path].value.items():
+ new_metrics[new_path].set(value, label_values + (match.group(1),))
+
+ self.metrics.update(new_metrics)
+
def collect(self):
# Clear the metrics before scraping
for k in self.metrics.keys():
)
self.metrics[path].set(value, labels)
+ self.add_fixed_name_metrics()
self.get_rbd_stats()
# Return formatted metrics and clear no longer used data