Merge pull request #68 from george-e-shaw-iv/master

Logging API Completed, Graceful Shutdown Fixed
This commit is contained in:
George Shaw 2017-11-10 16:47:35 -06:00 committed by GitHub
commit 71411191b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 445 additions and 99 deletions

8
.gitignore vendored
View file

@ -1,7 +1,11 @@
# Ignore executables generated
main
gpanel
# Ignore database files
datastore.db
datastore.db.lock
datastore.db.lock
# Ignore log files
logs/*
!logs/README.md

View file

@ -27,3 +27,9 @@ body {
cursor:pointer;
}
/* Navbar End */
/* General Start */
.btn {
cursor:pointer;
}
/* General End */

View file

@ -0,0 +1,50 @@
var logModal = jQuery('.view-log-modal');
jQuery('._js_diagnostics-clear-log').on('click', function(e){
e.preventDefault();
var logName = jQuery(this).attr('data');
var title;
switch(logName) {
case "client_errors":
title = "Client Error Log (4xx)";
break;
case "server_errors":
title = "Server Error Log (5xx)";
break;
case "load_time":
title = "Load Time Log";
break;
default:
return;
break;
}
var ensure = confirm('Are you sure you want to clear ' + title + '?');
if (ensure) {
var requestData = {};
requestData["name"] = logName+".log";
var xhr = new XMLHttpRequest();
xhr.open('UPDATE', 'api/logs/delete', true);
xhr.send(JSON.stringify(requestData));
xhr.onloadend = function() {
if(xhr.status == 204) {
jQuery(logModal).find('.modal-body textarea').html('The log is currently empty.');
}
else {
if(xhr.response != undefined && xhr.response.length != 0) {
alert(xhr.response);
}
else {
alert("An error has occurred, please contact your webhost administrator.");
}
}
}
}
else {
return;
}
});

View file

@ -0,0 +1,57 @@
var logModal = jQuery('.view-log-modal');
jQuery('._js_diagnostics-view-log').on('click', function(e){
e.preventDefault();
var logName = jQuery(this).attr('data');
var title;
switch(logName) {
case "client_errors":
title = "Client Error Log (4xx)";
break;
case "server_errors":
title = "Server Error Log (5xx)";
break;
case "load_time":
title = "Load Time Log";
break;
default:
return;
break;
}
jQuery(logModal).find('.modal-title').html(title);
jQuery(logModal).find('._js_diagnostics-clear-log').attr('data', logName);
var requestData = {};
requestData["name"] = logName+".log";
var xhr = new XMLHttpRequest();
xhr.open('POST', 'api/logs/read', true);
xhr.send(JSON.stringify(requestData));
xhr.onloadend = function() {
if(xhr.status == 200) {
var responseData;
if(xhr.response != undefined && xhr.response.length != 0) {
responseData = xhr.response;
}
else {
responseData = "The log is currently empty."
}
jQuery(logModal).find('.modal-body textarea').html(responseData);
jQuery(logModal).modal('show');
}
else {
if(xhr.response != undefined && xhr.response.length != 0) {
alert(xhr.response);
}
else {
alert('An error has occurred, please contact your webhost administrator.');
}
}
}
});

View file

@ -27,6 +27,26 @@
</div>
</nav>
<div class="modal fade view-log-modal" tabindex="-1" role="dialog" aria-labelledby="view-log-modal" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<textarea class="w-100 h-100" readonly></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger _js_diagnostics-clear-log" data="">Clear Log</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12">
@ -47,6 +67,20 @@
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-12">
<div class="card">
<div class="card-body">
<h4 class="card-title">Diagnostics</h4>
<h6 class="card-subtitle mb-4 text-muted">View various server data pools that may help pinpoint issues</h6>
<button type="button" class="btn btn-outline-primary _js_diagnostics-view-log" data="client_errors">Client Error Log</button>
<button type="button" class="btn btn-outline-primary _js_diagnostics-view-log" data="server_errors">Server Error Log</button>
<button type="button" class="btn btn-outline-primary _js_diagnostics-view-log" data="load_time">Load Time Log</button>
</div>
</div>
</div>
</div>
</div>
<footer class="sticky-footer">
@ -75,6 +109,9 @@
<script type="text/javascript" src="assets/js/panelHandlers/publicServer/start.js"></script>
<script type="text/javascript" src="assets/js/panelHandlers/publicServer/shutdown.js"></script>
<script type="text/javascript" src="assets/js/panelHandlers/publicServer/restart.js"></script>
<script type="text/javascript" src="assets/js/panelHandlers/diagnostics/viewLog.js"></script>
<script type="text/javascript" src="assets/js/panelHandlers/diagnostics/deleteLog.js"></script>
<!-- KEEP AT BOTTOM OF BODY TAGS -->
</body>
</html>

8
logs/README.md Normal file
View file

@ -0,0 +1,8 @@
# gPanel Logs
The actual log files are ignored by the .gitignore file. They are generated automatically upon being needed within the software itself.
Current log files:
- client_errors.log
- server_errors.log
- loadtime.log

View file

View file

View file

@ -5,6 +5,7 @@ import (
"net/http"
"strings"
"github.com/Ennovar/gPanel/pkg/api/logs"
"github.com/Ennovar/gPanel/pkg/api/server"
"github.com/Ennovar/gPanel/pkg/api/user"
"github.com/Ennovar/gPanel/pkg/public"
@ -39,6 +40,10 @@ func HandleAPI(res http.ResponseWriter, req *http.Request, path string, publicSe
return true, server.Restart(res, req, publicServer)
case "/server/maintenance":
return true, server.Maintenance(res, req, publicServer)
case "/logs/read":
return true, logs.Read(res, req)
case "/logs/delete":
return true, logs.Delete(res, req)
default:
return false, false
}

55
pkg/api/logs/delete.go Normal file
View file

@ -0,0 +1,55 @@
// Package logs is a child of package api to handle api calls concerning log files
package logs
import (
"encoding/json"
"net/http"
"github.com/Ennovar/gPanel/pkg/file"
)
func Delete(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "UPDATE" {
http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return false
}
var deleteLogRequestData struct {
Name string `json:"name"`
}
err := json.NewDecoder(req.Body).Decode(&deleteLogRequestData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
valid := false
for _, log := range file.KNOWN_LOGS {
if log == deleteLogRequestData.Name {
valid = true
break
}
}
if !valid {
http.Error(res, "Attempting to delete an unknown log file.", http.StatusBadRequest)
return false
}
handle, err := file.Open(deleteLogRequestData.Name, true, true)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
closeErr, deleteErr := handle.Close(true)
if closeErr != nil || deleteErr != nil {
http.Error(res, closeErr.Error()+" AND "+deleteErr.Error(), http.StatusInternalServerError)
return false
}
res.WriteHeader(http.StatusNoContent)
return true
}

57
pkg/api/logs/read.go Normal file
View file

@ -0,0 +1,57 @@
// Package logs is a child of package api to handle api calls concerning log files
package logs
import (
"encoding/json"
"net/http"
"github.com/Ennovar/gPanel/pkg/file"
)
func Read(res http.ResponseWriter, req *http.Request) bool {
if req.Method != "POST" {
http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return false
}
var readLogRequestData struct {
Name string `json:"name"`
}
err := json.NewDecoder(req.Body).Decode(&readLogRequestData)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
valid := false
for _, log := range file.KNOWN_LOGS {
if log == readLogRequestData.Name {
valid = true
break
}
}
if !valid {
http.Error(res, "Attempting to read from an unknown log file.", http.StatusBadRequest)
return false
}
handle, err := file.Open(readLogRequestData.Name, true, true)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
defer handle.Close(false)
data, err := handle.Read()
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false
}
res.WriteHeader(http.StatusOK)
res.Write(data)
return true
}

View file

@ -15,7 +15,7 @@ func Shutdown(res http.ResponseWriter, req *http.Request, publicServer *public.C
}
var shutdownRequestData struct {
graceful bool `json:"graceful"`
Graceful bool `json:"graceful"`
}
err := json.NewDecoder(req.Body).Decode(&shutdownRequestData)
@ -24,7 +24,7 @@ func Shutdown(res http.ResponseWriter, req *http.Request, publicServer *public.C
return false
}
err = publicServer.Stop(shutdownRequestData.graceful)
err = publicServer.Stop(shutdownRequestData.Graceful)
if err != nil {
http.Error(res, err.Error(), http.StatusBadRequest)
return false

123
pkg/file/file.go Normal file
View file

@ -0,0 +1,123 @@
// Package file handles various file operations
package file
import (
"io/ioutil"
"os"
"path/filepath"
)
const (
LOG_CLIENT_ERRORS = "client_errors.log"
LOG_SERVER_ERRORS = "server_errors.log"
LOG_LOADTIME = "load_time.log"
)
var KNOWN_LOGS = [...]string{LOG_CLIENT_ERRORS, LOG_SERVER_ERRORS, LOG_LOADTIME}
type Handler struct {
fileHandle *os.File
path string
append bool
}
func Open(file string, append bool, log bool) (*Handler, error) {
var err error
var absPath string
var f *os.File
// Generate file path
if log {
absPath, err = filepath.Abs("logs/" + file)
} else {
absPath, err = filepath.Abs(file)
}
// Handle file path errors
if err != nil {
return nil, err
}
// Open file
if append {
f, err = os.OpenFile(absPath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
} else {
f, err = os.OpenFile(absPath, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0644)
}
//Handle open file errors
if err != nil {
return nil, err
}
// Upon success, return the handler
return &Handler{
fileHandle: f,
path: absPath,
}, nil
}
func (h *Handler) checkExistence(createIfNotExist bool) (bool, error) {
if _, err := os.Stat(h.path); os.IsNotExist(err) {
if createIfNotExist {
if h.append {
h.fileHandle, err = os.OpenFile(h.path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
} else {
h.fileHandle, err = os.OpenFile(h.path, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0644)
}
if err != nil {
return false, err
}
return false, nil
} else {
return false, nil
}
}
return true, nil
}
func (h *Handler) Read() ([]byte, error) {
_, err := h.checkExistence(true)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(h.fileHandle)
if err != nil {
return nil, err
}
return data, err
}
func (h *Handler) Write(data string) (int, error) {
_, err := h.checkExistence(true)
if err != nil {
return 0, err
}
written, err := h.fileHandle.Write([]byte(data + "\n"))
if err != nil {
return 0, err
}
return written, err
}
func (h *Handler) Close(delete bool) (error, error) {
exist, _ := h.checkExistence(false)
if !exist {
return nil, nil
}
closeErr := h.fileHandle.Close()
if delete {
deleteErr := os.Remove(h.path)
return closeErr, deleteErr
}
return closeErr, nil
}

View file

@ -1,34 +0,0 @@
// Package logging handles the various mediums and logic of logging messages and reports.
package logging
import (
"log"
"strings"
"github.com/Ennovar/gPanel/pkg/networking"
)
const (
PRIVATE_PREFIX = "PRIVATE::"
PUBLIC_PREFIX = "PUBLIC::"
NORMAL_LOG = 1
FATAL_LOG = 2
)
// Console logs a prefix, IP, and message all appeneded together to the console.
func Console(prefix string, logType int, msg string) {
rawClientIP, _ := networking.GetClientIP()
clientIP := strings.TrimSpace(string(rawClientIP))
msg = prefix + clientIP + "::" + msg
switch logType {
default:
fallthrough
case 1:
log.Println(msg)
case 2:
log.Fatal(msg)
}
}

View file

@ -1,40 +0,0 @@
// Package logging handles the various mediums and logic of logging messages and reports.
package logging
import "os"
const (
PRIVATE_LOG_FOLDER = "logs/private/"
PUBLIC_LOG_FOLDER = "logs/public/"
ERROR = "error.log"
WARNING = "warning.log"
)
// File logs a given message to a log file
func File(logFolder string, logFile string, msg string) (string, error) {
msg += "\n"
f, err := os.OpenFile(logFolder+logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return "", err
} else {
writtenBytes, err := f.Write([]byte(msg))
if err != nil {
return string(writtenBytes), err
} else {
err := f.Close()
if err != nil {
return string(writtenBytes), err
} else {
return string(writtenBytes), nil
}
}
}
}

View file

@ -8,9 +8,10 @@ import (
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/Ennovar/gPanel/pkg/logging"
"github.com/Ennovar/gPanel/pkg/file"
"github.com/Ennovar/gPanel/pkg/routing"
)
@ -18,6 +19,9 @@ type Controller struct {
Directory string
GracefulShutdownTimeout time.Duration
Status int
ClientLogger *file.Handler
ServerLogger *file.Handler
LoadTimeLogger *file.Handler
}
var controller Controller
@ -25,10 +29,17 @@ var server http.Server
// New function returns a new PublicWeb type.
func New() *Controller {
clientLogHandler, _ := file.Open(file.LOG_CLIENT_ERRORS, true, true)
serverLogHandler, _ := file.Open(file.LOG_CLIENT_ERRORS, true, true)
loadLogHandler, _ := file.Open(file.LOG_LOADTIME, true, true)
controller = Controller{
Directory: "document_roots/public/",
GracefulShutdownTimeout: 5 * time.Second,
Status: 0,
ClientLogger: clientLogHandler,
ServerLogger: serverLogHandler,
LoadTimeLogger: loadLogHandler,
}
server = http.Server{
@ -60,18 +71,14 @@ func (con *Controller) Stop(graceful bool) error {
defer cancel()
err := server.Shutdown(context)
if err != nil {
fmt.Printf("Graceful shutdown failed attempting forced: %v\n", err)
err = server.Close()
if err != nil {
return err
}
if err == nil {
return nil
}
fmt.Printf("Graceful shutdown failed attempting forced: %v\n", err)
}
err := server.Close()
if err != nil {
if err := server.Close(); err != nil {
return err
}
@ -116,6 +123,8 @@ func (con *Controller) Maintenance() {
// ServeHTTP function routes all requests for the public web server. It is used in the main
// function inside of the http.ListenAndServe() function for the public host.
func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) {
startTime := time.Now()
switch con.Status {
case 0: // This will actually never show because this function won't run if the server is off
http.Error(res, "The server is currently down and not serving requests.", http.StatusServiceUnavailable)
@ -140,16 +149,16 @@ func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) {
f, err := os.Open(path)
if err != nil {
con.ClientLogger.Write(path + "::" + strconv.Itoa(http.StatusNotFound) + "::" + err.Error())
routing.HttpThrowStatus(http.StatusNotFound, res)
logging.Console(logging.PUBLIC_PREFIX, logging.NORMAL_LOG, "Path \""+path+"\" rendered a 404 error.")
return
}
contentType, err := routing.GetContentType(path)
if err != nil {
con.ClientLogger.Write(path + "::" + strconv.Itoa(http.StatusUnsupportedMediaType) + "::" + err.Error())
routing.HttpThrowStatus(http.StatusUnsupportedMediaType, res)
logging.Console(logging.PUBLIC_PREFIX, logging.NORMAL_LOG, "Path \""+path+"\" content type could not be determined, 404 error.")
return
}
@ -157,8 +166,11 @@ func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) {
_, err = io.Copy(res, f)
if err != nil {
con.ServerLogger.Write(path + "::" + strconv.Itoa(http.StatusInternalServerError) + "::" + err.Error())
routing.HttpThrowStatus(http.StatusInternalServerError, res)
logging.Console(logging.PUBLIC_PREFIX, logging.NORMAL_LOG, "Path \""+path+"\" rendered a 500 error.")
return
}
elapsedTime := time.Since(startTime)
con.LoadTimeLogger.Write(path + " rendered in " + strconv.FormatFloat(elapsedTime.Seconds(), 'f', 6, 64) + " seconds")
}

View file

@ -5,23 +5,28 @@ import (
"io"
"net/http"
"os"
"strconv"
"github.com/Ennovar/gPanel/pkg/api"
"github.com/Ennovar/gPanel/pkg/logging"
"github.com/Ennovar/gPanel/pkg/file"
"github.com/Ennovar/gPanel/pkg/public"
"github.com/Ennovar/gPanel/pkg/routing"
)
type Controller struct {
Directory string
Public *public.Controller
Directory string
Public *public.Controller
ServerLogger *file.Handler
}
// New returns a new PrivateHost type.
func New() Controller {
serverErrorLogger, _ := file.Open(file.LOG_SERVER_ERRORS, true, true)
return Controller{
Directory: "document_roots/webhost/",
Public: public.New(),
Directory: "document_roots/webhost/",
Public: public.New(),
ServerLogger: serverErrorLogger,
}
}
@ -37,6 +42,7 @@ func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) {
if reqAuth(path) {
if !checkAuth(res, req) {
con.ServerLogger.Write(path + "::" + strconv.Itoa(http.StatusUnauthorized) + "::" + http.StatusText(http.StatusUnauthorized))
http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
@ -52,16 +58,16 @@ func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) {
f, err := os.Open(path)
if err != nil {
con.ServerLogger.Write(path + "::" + strconv.Itoa(http.StatusNotFound) + "::" + err.Error())
routing.HttpThrowStatus(http.StatusNotFound, res)
logging.Console(logging.PRIVATE_PREFIX, logging.NORMAL_LOG, "Path \""+path+"\" rendered a 404 error.")
return
}
contentType, err := routing.GetContentType(path)
if err != nil {
con.ServerLogger.Write(path + "::" + strconv.Itoa(http.StatusUnsupportedMediaType) + "::" + err.Error())
routing.HttpThrowStatus(http.StatusUnsupportedMediaType, res)
logging.Console(logging.PUBLIC_PREFIX, logging.NORMAL_LOG, "Path \""+path+"\" content type could not be determined, 404 error.")
return
}
@ -69,8 +75,8 @@ func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) {
_, err = io.Copy(res, f)
if err != nil {
con.ServerLogger.Write(path + "::" + strconv.Itoa(http.StatusInternalServerError) + "::" + err.Error())
routing.HttpThrowStatus(http.StatusInternalServerError, res)
logging.Console(logging.PUBLIC_PREFIX, logging.NORMAL_LOG, "Path \""+path+"\" rendered a 500 error.")
return
}
}