PLT-12 adding log viewer

This commit is contained in:
=Corey Hulen
2015-09-10 18:32:22 -07:00
parent 41439eb801
commit e06e292be7
14 changed files with 338 additions and 15 deletions

51
api/admin.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"bufio"
"net/http"
"os"
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
func InitAdmin(r *mux.Router) {
l4g.Debug("Initializing admin api routes")
sr := r.PathPrefix("/admin").Subrouter()
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getLogs") {
return
}
var lines []string
if utils.Cfg.LogSettings.FileEnable {
file, err := os.Open(utils.Cfg.LogSettings.FileLocation)
if err != nil {
c.Err = model.NewAppError("getLogs", "Error reading log file", err.Error())
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
} else {
lines = append(lines, "")
}
w.Write([]byte(model.ArrayToJson(lines)))
}

35
api/admin_test.go Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"testing"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
)
func TestGetLogs(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
if logs, err := Client.GetLogs(); err != nil {
t.Fatal(err)
} else if len(logs.Data.([]string)) <= 0 {
t.Fatal()
}
}

View File

@@ -41,6 +41,7 @@ func InitApi() {
InitFile(r)
InitCommand(r)
InitConfig(r)
InitAdmin(r)
templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir)

View File

@@ -292,6 +292,16 @@ func (c *Context) IsSystemAdmin() bool {
return false
}
func (c *Context) HasSystemAdminPermissions(where string) bool {
if c.IsSystemAdmin() {
return true
}
c.Err = model.NewAppError(where, "You do not have the appropriate permissions", "userId="+c.Session.UserId)
c.Err.StatusCode = http.StatusForbidden
return false
}
func (c *Context) IsTeamAdmin(userId string) bool {
if uresult := <-Srv.Store.User().Get(userId); uresult.Err != nil {
c.Err = uresult.Err

View File

@@ -338,6 +338,15 @@ func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) {
}
}
func (c *Client) GetLogs() (*Result, *AppError) {
if r, err := c.DoGet("/admin/logs", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
}
}
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoPost("/channels/create", channel.ToJson()); err != nil {
return nil, err

View File

@@ -11,9 +11,10 @@ import (
)
const (
MODE_DEV = "dev"
MODE_BETA = "beta"
MODE_PROD = "prod"
MODE_DEV = "dev"
MODE_BETA = "beta"
MODE_PROD = "prod"
LOG_ROTATE_SIZE = 10000
)
type ServiceSettings struct {
@@ -180,10 +181,10 @@ func ConfigureCmdLineLog() {
ls.ConsoleEnable = true
ls.ConsoleLevel = "ERROR"
ls.FileEnable = false
configureLog(ls)
configureLog(&ls)
}
func configureLog(s LogSettings) {
func configureLog(s *LogSettings) {
l4g.Close()
@@ -217,7 +218,7 @@ func configureLog(s LogSettings) {
flw := l4g.NewFileLogWriter(s.FileLocation, false)
flw.SetFormat(s.FileFormat)
flw.SetRotate(true)
flw.SetRotateLines(100000)
flw.SetRotateLines(LOG_ROTATE_SIZE)
l4g.AddFilter("file", level, flw)
}
}
@@ -241,7 +242,7 @@ func LoadConfig(fileName string) {
panic("Error decoding config file=" + fileName + ", err=" + err.Error())
}
configureLog(config.LogSettings)
configureLog(&config.LogSettings)
Cfg = &config
SanitizeOptions = getSanitizeOptions()

View File

@@ -4,6 +4,7 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var EmailTab = require('./email_settings.jsx');
var JobsTab = require('./jobs_settings.jsx');
var LogsTab = require('./logs.jsx');
var Navbar = require('../../components/navbar.jsx');
export default class AdminController extends React.Component {
@@ -28,6 +29,8 @@ export default class AdminController extends React.Component {
tab = <EmailTab />;
} else if (this.state.selected === 'job_settings') {
tab = <JobsTab />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
}
return (

View File

@@ -83,7 +83,15 @@ export default class AdminSidebar extends React.Component {
{'Email Settings'}
</a>
</li>
<li><a href='#'>{'Other Settings'}</a></li>
<li>
<a
href='#'
className={this.isSelected('logs')}
onClick={this.handleClick.bind(null, 'logs')}
>
{'Logs'}
</a>
</li>
</ul>
</li>
<li>

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AdminStore = require('../../stores/admin_store.jsx');
var LoadingScreen = require('../loading_screen.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class Logs extends React.Component {
constructor(props) {
super(props);
this.onLogListenerChange = this.onLogListenerChange.bind(this);
this.reload = this.reload.bind(this);
this.state = {
logs: AdminStore.getLogs()
};
}
componentDidMount() {
AdminStore.addLogChangeListener(this.onLogListenerChange);
AsyncClient.getLogs();
}
componentWillUnmount() {
AdminStore.removeLogChangeListener(this.onLogListenerChange);
}
onLogListenerChange() {
this.setState({
logs: AdminStore.getLogs()
});
}
reload() {
AdminStore.saveLogs(null);
this.setState({
logs: null
});
AsyncClient.getLogs();
}
render() {
var content = null;
if (this.state.logs === null) {
content = <LoadingScreen />;
} else {
content = [];
for (var i = 0; i < this.state.logs.length; i++) {
var style = {
whiteSpace: 'nowrap',
fontFamily: 'monospace'
};
if (this.state.logs[i].indexOf('[EROR]') > 0) {
style.color = 'red';
}
content.push(<br key={'br_' + i} />);
content.push(
<span
key={'log_' + i}
style={style}
>
{this.state.logs[i]}
</span>
);
}
}
var divStyle = {
overflow: 'scroll',
width: '100%',
height: '800px',
border: '1px solid #ddd',
marginTop: '10px',
padding: '5px',
backgroundColor: 'white'
};
return (
<div className='panel'>
<h3>{'Server Logs'}</h3>
<button
type='submit'
className='btn btn-primary'
onClick={this.reload}
>
{'Reload'}
</button>
<div style={divStyle}>
{content}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var BrowserStore = require('../stores/browser_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
class AdminStoreClass extends EventEmitter {
constructor() {
super();
this.logs = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
}
emitLogChange() {
this.emit(LOG_CHANGE_EVENT);
}
addLogChangeListener(callback) {
this.on(LOG_CHANGE_EVENT, callback);
}
removeLogChangeListener(callback) {
this.removeListener(LOG_CHANGE_EVENT, callback);
}
getLogs() {
//return BrowserStore.getItem('logs');
return this.logs;
}
saveLogs(logs) {
// if (logs === null) {
// BrowserStore.removeItem('logs');
// } else {
// BrowserStore.setItem('logs', logs);
// }
this.logs = logs;
}
}
var AdminStore = new AdminStoreClass();
AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
case ActionTypes.RECIEVED_LOGS:
AdminStore.saveLogs(action.logs);
AdminStore.emitLogChange();
break;
default:
}
});
export default AdminStore;

View File

@@ -319,6 +319,32 @@ export function getAudits() {
);
}
export function getLogs() {
if (isCallInProgress('getLogs')) {
return;
}
callTracker.getLogs = utils.getTimestamp();
client.getLogs(
(data, textStatus, xhr) => {
callTracker.getLogs = 0;
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_LOGS,
logs: data
});
},
(err) => {
callTracker.getLogs = 0;
dispatchError(err, 'getLogs');
}
);
}
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;

View File

@@ -294,6 +294,20 @@ export function getAudits(userId, success, error) {
});
}
export function getLogs(success, error) {
$.ajax({
url: '/api/v1/admin/logs',
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
error: function onError(xhr, status, err) {
var e = handleError('getLogs', xhr, status, err);
error(e);
}
});
}
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({

View File

@@ -34,7 +34,9 @@ module.exports = {
CLICK_TEAM: null,
RECIEVED_TEAM: null,
RECIEVED_CONFIG: null
RECIEVED_CONFIG: null,
RECIEVED_LOGS: null
}),
PayloadSources: keyMirror({

View File

@@ -643,12 +643,10 @@ func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request)
func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.Err = model.NewAppError("adminConsole", "You do not have permission to access the admin console.", "")
c.Err.StatusCode = http.StatusForbidden
if !c.HasSystemAdminPermissions("adminConsole") {
return
} else {
page := NewHtmlTemplatePage("admin_console", "Admin Console")
page.Render(c, w)
}
page := NewHtmlTemplatePage("admin_console", "Admin Console")
page.Render(c, w)
}