Implement Zaqar logger adapter

Implements: blueprint websocket-logging
Change-Id: Ie5e2554b54512ee0b590efa57a4269806b0f9294
This commit is contained in:
Honza Pokorny 2017-04-24 10:22:58 -03:00
parent 1c665a1d50
commit 8420610ec2
29 changed files with 450 additions and 88 deletions

View File

@ -24,5 +24,6 @@ window.tripleOUiConfig = {
// 'excludedLanguages': ['de', 'ja'],
// Logging
// 'loggers': ['console']
// 'loggers': ['console', 'zaqar']
// 'logger-zaqar-queue': 'tripleo-ui-logging'
};

View File

@ -0,0 +1,5 @@
---
features:
- |
Implements `websocket-logging <https://blueprints.launchpad.net/openstack/?searchtext=websocket-logging>`__
Implement Zaqar logger adapter

View File

@ -19,10 +19,10 @@ import { Map } from 'immutable';
import React from 'react';
import ReactShallowRenderer from 'react-test-renderer/shallow';
import store from '../../../js/store';
import PlansList from '../../../js/components/plan/PlansList';
import FileList from '../../../js/components/plan/FileList';
import { PlanFile } from '../../../js/immutableRecords/plans';
import store from '../../../js/store';
describe('PlansList component', () => {
let output;

View File

@ -0,0 +1,63 @@
/**
* Copyright 2017 Red Hat Inc.
*
* 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 { List, Map } from 'immutable';
import { InitialLoggerState } from '../../js/immutableRecords/logger';
import LoggerActions from '../../js/actions/LoggerActions';
import loggerReducer from '../../js/reducers/loggerReducer';
describe('loggerReducer state', () => {
describe('default state', () => {
let state;
beforeEach(() => {
state = loggerReducer(undefined, { type: 'undefined-action' });
});
it('`authenticated` is false', () => {
expect(state.get('authenticated')).toBe(false);
});
it('`messages` is empty', () => {
expect(state.get('messages').isEmpty()).toBe(true);
});
});
describe('QUEUE_MESSAGE', () => {
it('enqueues a messages', () => {
let state = loggerReducer(
new InitialLoggerState(),
LoggerActions.queueMessage(1)
);
expect(state.get('messages').size).toEqual(1);
});
});
describe('FLUSH_MESSAGES_SUCCESS', () => {
it('flushes messages', () => {
let state = loggerReducer(
Map({
messages: List([1, 2, 3]),
authenticated: true
}),
LoggerActions.flushMessagesSuccess()
);
expect(state.get('messages').isEmpty()).toBe(true);
});
});
});

View File

@ -25,7 +25,7 @@ import NotificationActions from '../actions/NotificationActions';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
import { topicSchema } from '../normalizrSchemas/environmentConfiguration';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
import SwiftApiErrorHandler from '../services/SwiftApiErrorHandler';
import SwiftApiService from '../services/SwiftApiService';

View File

@ -0,0 +1,67 @@
/**
* Copyright 2017 Red Hat Inc.
*
* 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 when from 'when';
import LoggerConstants from '../constants/LoggerConstants';
import ZaqarWebSocketService from '../services/ZaqarWebSocketService';
import NotificationActions from '../actions/NotificationActions';
export default {
queueMessage(message) {
return {
type: LoggerConstants.QUEUE_MESSAGE,
payload: message
};
},
flushMessagesSuccess() {
return {
type: LoggerConstants.FLUSH_MESSAGES_SUCCESS
};
},
flushMessages() {
return (dispatch, getState) => {
const messages = getState().logger.messages;
when
.all(() => {
messages.map(message => {
ZaqarWebSocketService.sendMessage('message_post', message);
});
})
.then(() => {
dispatch(this.flushMessagesSuccess());
})
.catch(error => {
// We're using `console` here to avoid circular imports.
console.error(error); // eslint-disable-line no-console
dispatch(
NotificationActions.notify({
title: 'Logging error',
message: 'Failed to flush Zaqar messages.'
})
);
});
};
},
authenticated() {
return {
type: LoggerConstants.WS_AUTHENTICATION_SUCCESS
};
}
};

View File

@ -20,7 +20,7 @@ import KeystoneApiErrorHandler from '../services/KeystoneApiErrorHandler';
import KeystoneApiService from '../services/KeystoneApiService';
import LoginConstants from '../constants/LoginConstants';
import ZaqarWebSocketService from '../services/ZaqarWebSocketService';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
import cookie from 'react-cookie';
export default {

View File

@ -34,7 +34,7 @@ import {
introspectionStatusSchema
} from '../normalizrSchemas/nodes';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
import { setNodeCapability } from '../utils/nodes';
const messages = defineMessages({

View File

@ -22,7 +22,7 @@ import ParametersConstants from '../constants/ParametersConstants';
import MistralApiService from '../services/MistralApiService';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
const messages = defineMessages({
parametersUpdatedNotficationTitle: {

View File

@ -19,7 +19,7 @@ import { fromJS } from 'immutable';
import { normalize, arrayOf } from 'normalizr';
import when from 'when';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
import MistralApiService from '../services/MistralApiService';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
import NotificationActions from '../actions/NotificationActions';

View File

@ -27,7 +27,7 @@ import NodesActions from './NodesActions';
import { nodeSchema } from '../normalizrSchemas/nodes';
import ValidationsActions from './ValidationsActions';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
const messages = defineMessages({
registrationNotificationTitle: {

View File

@ -19,7 +19,7 @@ import RolesConstants from '../constants/RolesConstants';
import MistralApiService from '../services/MistralApiService';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
export default {
fetchRoles(planName) {

View File

@ -21,7 +21,7 @@ import HeatApiService from '../services/HeatApiService';
import NotificationActions from '../actions/NotificationActions';
import StacksConstants from '../constants/StacksConstants';
import { stackSchema, stackResourceSchema } from '../normalizrSchemas/stacks';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
export default {
fetchStacksPending() {

View File

@ -24,7 +24,7 @@ import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
import ValidationsConstants from '../constants/ValidationsConstants';
import { validationSchema } from '../normalizrSchemas/validations';
import MistralConstants from '../constants/MistralConstants';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
export default {
fetchValidations() {

View File

@ -24,7 +24,7 @@ import WorkflowExecutionsConstants
import {
workflowExecutionSchema
} from '../normalizrSchemas/workflowExecutions';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
export default {
fetchWorkflowExecutions() {

View File

@ -14,6 +14,8 @@
* under the License.
*/
import { get } from 'lodash';
import LoggerActions from './LoggerActions';
import NodesActions from './NodesActions';
import PlansActions from './PlansActions';
import RegisterNodesActions from './RegisterNodesActions';
@ -28,8 +30,18 @@ export default {
};
},
handleAuthenticationSuccess(message, dispatch) {
message = get(message, ['body', 'message']);
if (message === 'Authentified.') {
dispatch(LoggerActions.authenticated());
dispatch(LoggerActions.flushMessages());
}
},
messageReceived(message, history) {
return (dispatch, getState) => {
this.handleAuthenticationSuccess(message, dispatch);
const { type, payload } = message.body;
switch (type) {
case MistralConstants.BAREMETAL_REGISTER_OR_UPDATE:
@ -74,5 +86,31 @@ export default {
break;
}
};
},
postMessage(queueName, body, ttl = 3600) {
return (dispatch, getState) => {
const message = {
queue_name: queueName,
messages: [
{
body,
ttl
}
]
};
// Drop the message on the floor when there is no `store`
if (!getState) {
return;
}
if (!getState().logger.authenticated) {
dispatch(LoggerActions.queueMessage(message));
return;
}
ZaqarWebSocketService.sendMessage('message_post', message);
};
}
};

View File

@ -42,9 +42,9 @@ const messages = defineMessages({
class AuthenticatedContent extends React.Component {
componentDidMount() {
this.props.initializeZaqarConnection();
this.props.fetchPlans();
this.props.fetchWorkflowExecutions();
this.props.initializeZaqarConnection();
}
render() {

View File

@ -0,0 +1,23 @@
/**
* Copyright 2017 Red Hat Inc.
*
* 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 keyMirror from 'keymirror';
export default keyMirror({
QUEUE_MESSAGE: null,
FLUSH_MESSAGES_SUCCESS: null,
WS_AUTHENTICATION_SUCCESS: null
});

View File

@ -14,8 +14,11 @@
* under the License.
*/
let appConfig = window.tripleOUiConfig || {};
import { getAppConfig } from '../services/utils';
let zaqarDefaultQueue = appConfig.zaqar_default_queue || 'tripleo';
let zaqarDefaultQueue = getAppConfig()['zaqar_default_queue'] || 'tripleo';
let zaqarLoggingQueue =
getAppConfig()['logger-zaqar-queue'] || 'tripleo-ui-logging';
export const ZAQAR_DEFAULT_QUEUE = zaqarDefaultQueue;
export const ZAQAR_LOGGING_QUEUE = zaqarLoggingQueue;

View File

@ -0,0 +1,22 @@
/**
* Copyright 2017 Red Hat Inc.
*
* 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 { List, Record } from 'immutable';
export const InitialLoggerState = Record({
messages: List(),
authenticated: false
});

View File

@ -20,6 +20,7 @@ import { reducer as formReducer } from 'redux-form';
import environmentConfigurationReducer from './environmentConfigurationReducer';
import filtersReducer from './filtersReducer';
import i18nReducer from './i18nReducer';
import loggerReducer from './loggerReducer';
import loginReducer from './loginReducer';
import nodesReducer from './nodesReducer';
import notificationsReducer from './notificationsReducer';
@ -36,6 +37,7 @@ const appReducer = combineReducers({
executions: workflowExecutionsReducer,
filters: filtersReducer,
i18n: i18nReducer,
logger: loggerReducer,
login: loginReducer,
nodes: nodesReducer,
notifications: notificationsReducer,

View File

@ -0,0 +1,39 @@
/**
* Copyright 2017 Red Hat Inc.
*
* 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 { List } from 'immutable';
import { InitialLoggerState } from '../immutableRecords/logger';
import LoggerConstants from '../constants/LoggerConstants';
const initialState = new InitialLoggerState();
export default function loggerReduder(state = initialState, action) {
switch (action.type) {
case LoggerConstants.QUEUE_MESSAGE:
return state.update('messages', messages =>
messages.push(action.payload)
);
case LoggerConstants.WS_AUTHENTICATION_SUCCESS:
return state.set('authenticated', true);
case LoggerConstants.FLUSH_MESSAGES_SUCCESS:
return state.set('messages', List());
default:
return state;
}
}

View File

@ -19,7 +19,7 @@ import request from 'reqwest';
import when from 'when';
import { getAuthTokenId, getServiceUrl } from '../services/utils';
import logger from '../services/logger';
import logger from '../services/logging/LoggingService';
class SwiftApiService {
defaultRequest(path, additionalAttributes) {

View File

@ -21,7 +21,13 @@ import { getAuthTokenId, getProjectId, getServiceUrl } from './utils';
import { ZAQAR_DEFAULT_QUEUE } from '../constants/ZaqarConstants';
import ZaqarActions from '../actions/ZaqarActions';
import NotificationActions from '../actions/NotificationActions';
import logger from '../services/logger';
// We're using `console` here to avoid circular imports.
const logger = {
error: (...msg) => {
console.log(...msg); // eslint-disable-line no-console
}
};
export default {
socket: null,
@ -55,7 +61,8 @@ export default {
};
this.socket.onmessage = evt => {
dispatch(ZaqarActions.messageReceived(JSON.parse(evt.data), history));
const data = JSON.parse(evt.data);
dispatch(ZaqarActions.messageReceived(data, history));
};
});
},

View File

@ -25,66 +25,17 @@
//
// Usage:
//
// import logger from 'src/js/services/logger';
// import logger from 'src/js/services/logging/LoggingService';
// logger.log('Hello world!');
class Adapter {
debug(...args) {}
info(...args) {}
warn(...args) {}
error(...args) {}
group(...args) {}
groupCollapsed(...args) {}
groupEnd(...args) {}
log(...args) {}
}
class ConsoleAdapter extends Adapter {
debug(...args) {
console.debug(...args); // eslint-disable-line no-console
}
info(...args) {
console.info(...args); // eslint-disable-line no-console
}
warn(...args) {
console.warn(...args); // eslint-disable-line no-console
}
error(...args) {
console.error(...args); // eslint-disable-line no-console
}
group(...args) {
console.group(...args); // eslint-disable-line no-console
}
groupCollapsed(...args) {
console.groupCollapsed(...args); // eslint-disable-line no-console
}
groupEnd(...args) {
console.groupEnd(...args); // eslint-disable-line no-console
}
log(...args) {
console.log(...args); // eslint-disable-line no-console
}
}
class ZaqarAdapter extends Adapter {}
import { includes } from 'lodash';
import LoggerConstants from '../../constants/LoggerConstants';
import ConsoleAdapter from './adapters/ConsoleAdapter';
import ZaqarAdapter from './adapters/ZaqarAdapter';
const AVAILABLE_ADAPTERS = {
console: new ConsoleAdapter(),
zaqar: new ZaqarAdapter()
console: ConsoleAdapter,
zaqar: ZaqarAdapter
};
class Logger {
@ -101,10 +52,7 @@ class Logger {
constructor() {
this.adapters = [];
if (window.tripleOUiConfig !== undefined) {
this.loadAdapters();
}
this.reduxDispatch = null;
this.AVAILABLE_FUNCTIONS.forEach(fn => {
this[fn] = function(...args) {
@ -115,19 +63,20 @@ class Logger {
this.registerGlobalErrorHandler();
}
setReduxDispatch(dispatchFunction) {
this.reduxDispatch = dispatchFunction;
this.loadAdapters();
}
loadAdapters() {
if (this.adapters.length) {
return;
}
if (window.tripleOUiConfig === undefined) {
return;
}
let enabledAdapters = window.tripleOUiConfig.loggers || ['console'];
let enabledAdapters = (window.tripleOUiConfig || {}).loggers || ['console'];
enabledAdapters.forEach(adapter => {
let instance = AVAILABLE_ADAPTERS[adapter];
let instance = new AVAILABLE_ADAPTERS[adapter](this.reduxDispatch);
if (instance === undefined) {
throw Error(`Adapter ${adapter} not defined`);
@ -138,8 +87,6 @@ class Logger {
}
dispatch(fn, ...args) {
this.loadAdapters();
this.adapters.forEach(adapter => {
let f = adapter[fn];
@ -159,4 +106,11 @@ class Logger {
}
}
const isLoggerAction = action => includes(LoggerConstants, action.type);
// The `predicate` prevents redux-logger from logging logger messages
// in order to avoid an infinite loop. This function is used in `createLogger`
// in store.js.
export const predicate = (getState, action) => !isLoggerAction(action);
export default new Logger();

View File

@ -0,0 +1,21 @@
export default class Adapter {
constructor(dispatch) {
this._dispatch = dispatch;
}
debug(...args) {}
info(...args) {}
warn(...args) {}
error(...args) {}
group(...args) {}
groupCollapsed(...args) {}
groupEnd(...args) {}
log(...args) {}
}

View File

@ -0,0 +1,35 @@
import Adapter from './BaseAdapter';
export default class ConsoleAdapter extends Adapter {
debug(...args) {
console.debug(...args); // eslint-disable-line no-console
}
info(...args) {
console.info(...args); // eslint-disable-line no-console
}
warn(...args) {
console.warn(...args); // eslint-disable-line no-console
}
error(...args) {
console.error(...args); // eslint-disable-line no-console
}
group(...args) {
console.group(...args); // eslint-disable-line no-console
}
groupCollapsed(...args) {
console.groupCollapsed(...args); // eslint-disable-line no-console
}
groupEnd(...args) {
console.groupEnd(...args); // eslint-disable-line no-console
}
log(...args) {
console.log(...args); // eslint-disable-line no-console
}
}

View File

@ -0,0 +1,70 @@
import { ZAQAR_LOGGING_QUEUE } from '../../../constants/ZaqarConstants';
import ZaqarActions from '../../../actions/ZaqarActions';
import Adapter from './BaseAdapter';
export default class ZaqarAdapter extends Adapter {
constructor(dispatch) {
super(dispatch);
this.indent = 0;
this.buffer = [];
}
_formatMessage(message, level) {
return {
message,
level,
timestamp: Date.now()
};
}
_send(message, level) {
if (this.indent !== 0) {
this.buffer.push({
message,
level
});
return;
}
const msg = this._formatMessage(message[0], level);
this._dispatch(ZaqarActions.postMessage(ZAQAR_LOGGING_QUEUE, msg));
}
debug(...args) {
this._send(args, 'debug');
}
info(...args) {
this._send(args, 'info');
}
warn(...args) {
this._send(args, 'warn');
}
error(...args) {
this._send(args, 'error');
}
group(...args) {
this.indent++;
}
groupCollapsed(...args) {
this.indent++;
}
groupEnd(...args) {
this.indent--;
this.buffer.map(m => {
this._send(m.message, m.level);
});
this.buffer = [];
}
log(...args) {
this.info(args);
}
}

View File

@ -18,7 +18,7 @@ import { applyMiddleware, createStore } from 'redux';
import cookie from 'react-cookie';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import logger from './services/logger';
import logger, { predicate } from './services/logging/LoggingService';
import appReducer from './reducers/appReducer';
import { InitialPlanState } from './immutableRecords/plans';
@ -44,7 +44,17 @@ function getStoredPlanName() {
const loggerMiddleware = createLogger({
collapsed: true,
logger: logger
predicate: predicate,
logger: logger,
// We're turning off all colors here because the formatting chars obscure the
// content server-side.
colors: {
title: false,
prevState: false,
action: false,
nextState: false,
error: false
}
});
const store = createStore(
@ -56,4 +66,6 @@ const store = createStore(
)
);
logger.setReduxDispatch(store.dispatch);
export default store;