From e06e292be71ca699d90bafbd635118aa47c2d7a5 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Thu, 10 Sep 2015 18:32:22 -0700 Subject: PLT-12 adding log viewer --- api/admin.go | 51 +++++++++++ api/admin_test.go | 35 ++++++++ api/api.go | 1 + api/context.go | 10 +++ model/client.go | 9 ++ utils/config.go | 15 ++-- .../components/admin_console/admin_controller.jsx | 3 + .../components/admin_console/admin_sidebar.jsx | 10 ++- web/react/components/admin_console/logs.jsx | 98 ++++++++++++++++++++++ web/react/stores/admin_store.jsx | 67 +++++++++++++++ web/react/utils/async_client.jsx | 26 ++++++ web/react/utils/client.jsx | 14 ++++ web/react/utils/constants.jsx | 4 +- web/web.go | 10 +-- 14 files changed, 338 insertions(+), 15 deletions(-) create mode 100644 api/admin.go create mode 100644 api/admin_test.go create mode 100644 web/react/components/admin_console/logs.jsx create mode 100644 web/react/stores/admin_store.jsx diff --git a/api/admin.go b/api/admin.go new file mode 100644 index 000000000..d4af1d247 --- /dev/null +++ b/api/admin.go @@ -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))) +} diff --git a/api/admin_test.go b/api/admin_test.go new file mode 100644 index 000000000..460ac1208 --- /dev/null +++ b/api/admin_test.go @@ -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() + } +} diff --git a/api/api.go b/api/api.go index 9770930f7..35ac0bdc0 100644 --- a/api/api.go +++ b/api/api.go @@ -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) diff --git a/api/context.go b/api/context.go index 1852ed4d6..ea5677f95 100644 --- a/api/context.go +++ b/api/context.go @@ -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 diff --git a/model/client.go b/model/client.go index c355b90f5..5aac09289 100644 --- a/model/client.go +++ b/model/client.go @@ -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 diff --git a/utils/config.go b/utils/config.go index c67e17e79..a1d282c29 100644 --- a/utils/config.go +++ b/utils/config.go @@ -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() diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index bb43af802..68984c9e0 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -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 = ; } else if (this.state.selected === 'job_settings') { tab = ; + } else if (this.state.selected === 'logs') { + tab = ; } return ( diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 6b3be89d0..a04bceef5 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -83,7 +83,15 @@ export default class AdminSidebar extends React.Component { {'Email Settings'} -
  • {'Other Settings'}
  • +
  • + + {'Logs'} + +
  • diff --git a/web/react/components/admin_console/logs.jsx b/web/react/components/admin_console/logs.jsx new file mode 100644 index 000000000..959d99a30 --- /dev/null +++ b/web/react/components/admin_console/logs.jsx @@ -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 = ; + } 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(
    ); + content.push( + + {this.state.logs[i]} + + ); + } + } + + var divStyle = { + overflow: 'scroll', + width: '100%', + height: '800px', + border: '1px solid #ddd', + marginTop: '10px', + padding: '5px', + backgroundColor: 'white' + }; + + return ( +
    +

    {'Server Logs'}

    + +
    + {content} +
    +
    + ); + } +} \ No newline at end of file diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx new file mode 100644 index 000000000..ac474cd4b --- /dev/null +++ b/web/react/stores/admin_store.jsx @@ -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; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 6ccef0506..6b8e73c5a 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -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; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 51fd16474..75ffdb274 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -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({ diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 7ead079d7..03e4635b5 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -34,7 +34,9 @@ module.exports = { CLICK_TEAM: null, RECIEVED_TEAM: null, - RECIEVED_CONFIG: null + RECIEVED_CONFIG: null, + + RECIEVED_LOGS: null }), PayloadSources: keyMirror({ diff --git a/web/web.go b/web/web.go index 9cb81226b..1ed055a62 100644 --- a/web/web.go +++ b/web/web.go @@ -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) } -- cgit v1.2.3-1-g7c22 From 9a3f67a9c94a3c06c02086ec5c5fd3d4307d4301 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 14 Sep 2015 17:18:47 -0700 Subject: Fixing eslint issues --- web/react/stores/admin_store.jsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index ac474cd4b..591b52d05 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -4,8 +4,6 @@ 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; @@ -35,17 +33,10 @@ class AdminStoreClass extends EventEmitter { } getLogs() { - //return BrowserStore.getItem('logs'); return this.logs; } saveLogs(logs) { - // if (logs === null) { - // BrowserStore.removeItem('logs'); - // } else { - // BrowserStore.setItem('logs', logs); - // } - this.logs = logs; } } -- cgit v1.2.3-1-g7c22 From 23a1311ef5f9ecac0a250419a49e74b290e63516 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 15 Sep 2015 09:19:29 -0700 Subject: Moving style into css --- mattermost.go | 6 +++--- web/react/components/admin_console/logs.jsx | 12 +----------- web/sass-files/sass/partials/_admin-console.scss | 10 ++++++++++ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mattermost.go b/mattermost.go index 499abcd92..c95c8ee43 100644 --- a/mattermost.go +++ b/mattermost.go @@ -329,9 +329,9 @@ Usage: platform -create_user -team_name="name" -email="user@example.com" -password="mypassword" -assign_role Assigns role to a user. It requres the -role, - -email and -team_name flag. If you're assigning the - role="system_admin" role it must be for a user on the - team_name="admin" + -email and -team_name flag. You may need to logout + of your current sessions for the new role to be + applied. Example: platform -assign_role -team_name="name" -email="user@example.com" -role="admin" diff --git a/web/react/components/admin_console/logs.jsx b/web/react/components/admin_console/logs.jsx index 959d99a30..d7de76a94 100644 --- a/web/react/components/admin_console/logs.jsx +++ b/web/react/components/admin_console/logs.jsx @@ -69,16 +69,6 @@ export default class Logs extends React.Component { } } - var divStyle = { - overflow: 'scroll', - width: '100%', - height: '800px', - border: '1px solid #ddd', - marginTop: '10px', - padding: '5px', - backgroundColor: 'white' - }; - return (

    {'Server Logs'}

    @@ -89,7 +79,7 @@ export default class Logs extends React.Component { > {'Reload'} -
    +
    {content}
    diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index b32cc1218..9823d2611 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -73,6 +73,16 @@ } } +.log__panel { + overflow: scroll; + width: 100%; + height: 800px; + border: 1px solid #ddd; + margin-top: 10px; + padding: 5px; + background-color: white; +} + .app__content { &.admin { overflow: auto; -- cgit v1.2.3-1-g7c22