Skip to content
Snippets Groups Projects
Commit 43fc8113 authored by Tom Robinson's avatar Tom Robinson
Browse files

In-app logs prototype

parent c9d606fa
No related branches found
No related tags found
No related merge requests found
import React, { Component, PropTypes } from "react";
import ReactDOM from "react-dom";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
import reactAnsiStyle from "react-ansi-style";
import "react-ansi-style/inject-css";
import _ from "underscore";
export default class Logs extends Component {
constructor(props, context) {
super(props, context);
this.state = {
logs: [],
scrollToBottom: true
};
this._onScroll = () => {
this.scrolling = true;
this._onScrollDebounced();
}
this._onScrollDebounced = _.debounce(() => {
let elem = ReactDOM.findDOMNode(this).parentNode;
let scrollToBottom = Math.abs(elem.scrollTop - (elem.scrollHeight - elem.offsetHeight)) < 10;
this.setState({ scrollToBottom }, () => {
this.scrolling = false;
});
}, 500);
}
componentWillMount() {
this.timer = setInterval(async () => {
let response = await fetch("/api/log", { credentials: 'same-origin' });
let logs = await response.json()
this.setState({ logs: logs.reverse() })
}, 1000);
}
componentDidMount() {
let elem = ReactDOM.findDOMNode(this).parentNode;
elem.addEventListener("scroll", this._onScroll, false);
}
componentDidUpdate() {
let elem = ReactDOM.findDOMNode(this).parentNode;
if (!this.scrolling && this.state.scrollToBottom) {
if (elem.scrollTop !== elem.scrollHeight - elem.offsetHeight) {
elem.scrollTop = elem.scrollHeight - elem.offsetHeight;
}
}
}
componentWillUnmount() {
let elem = ReactDOM.findDOMNode(this).parentNode;
elem.removeEventListener("scroll", this._onScroll, false);
clearTimeout(this.timer);
}
render() {
let { logs } = this.state;
return (
<LoadingAndErrorWrapper loading={!logs || logs.length === 0}>
{() =>
<div style={{ backgroundColor: "black", fontFamily: "monospace", fontSize: "14px", whiteSpace: "pre-line", padding: "0.5em" }}>
{reactAnsiStyle(React, logs.join("\n"))}
</div>
}
</LoadingAndErrorWrapper>
);
}
}
......@@ -5,6 +5,7 @@ import _ from "underscore";
import MetabaseSettings from "metabase/lib/settings";
import Modal from "metabase/components/Modal.jsx";
import Logs from "metabase/components/Logs.jsx";
import UserAvatar from './UserAvatar.jsx';
import Icon from './Icon.jsx';
......@@ -16,7 +17,10 @@ export default class ProfileLink extends Component {
constructor(props, context) {
super(props, context);
this.state = { dropdownOpen: false, aboutModalOpen: false };
this.state = {
dropdownOpen: false,
modalOpen: null
};
_.bindAll(this, "toggleDropdown", "closeDropdown", "openModal", "closeModal");
}
......@@ -34,17 +38,17 @@ export default class ProfileLink extends Component {
this.setState({ dropdownOpen: false });
}
openModal() {
this.setState({ dropdownOpen: false, aboutModalOpen: true });
openModal(modalName) {
this.setState({ dropdownOpen: false, modalOpen: modalName });
}
closeModal() {
this.setState({ aboutModalOpen: false });
this.setState({ modalOpen: null });
}
render() {
const { user, context } = this.props;
const { aboutModalOpen, dropdownOpen } = this.state;
const { modalOpen, dropdownOpen } = this.state;
const { tag, date } = MetabaseSettings.get('version');
let dropDownClasses = cx({
......@@ -98,7 +102,13 @@ export default class ProfileLink extends Component {
</li>
<li>
<a data-metabase-event={"Navbar;Profile Dropdown;About "+tag} onClick={this.openModal} className="Dropdown-item block text-white no-decoration">
<a data-metabase-event={"Navbar;Profile Dropdown;Debugging "+tag} onClick={this.openModal.bind(this, "logs")} className="Dropdown-item block text-white no-decoration">
Logs
</a>
</li>
<li>
<a data-metabase-event={"Navbar;Profile Dropdown;About "+tag} onClick={this.openModal.bind(this, "about")} className="Dropdown-item block text-white no-decoration">
About Metabase
</a>
</li>
......@@ -110,7 +120,7 @@ export default class ProfileLink extends Component {
</div>
: null }
{ aboutModalOpen ?
{ modalOpen === "about" ?
<Modal className="Modal Modal--small">
<div className="px4 pt4 pb2 text-centered relative">
<span className="absolute top right p4 text-normal text-grey-3 cursor-pointer" onClick={this.closeModal}>
......@@ -130,6 +140,10 @@ export default class ProfileLink extends Component {
<span>and is built with care in San Francisco, CA</span>
</div>
</Modal>
: modalOpen === "logs" ?
<Modal className="Modal">
<Logs />
</Modal>
: null }
</div>
</OnClickOut>
......
......@@ -109,7 +109,8 @@
"-Xms1024m" ; give JVM a decent heap size to start with
"-Xmx2048m" ; hard limit of 2GB so we stop hitting the 4GB container limit on CircleCI
"-XX:+CMSClassUnloadingEnabled" ; let Clojure's dynamically generated temporary classes be GC'ed from PermGen
"-XX:+UseConcMarkSweepGC"]} ; Concurrent Mark Sweep GC needs to be used for Class Unloading (above)
"-XX:+UseConcMarkSweepGC"] ; Concurrent Mark Sweep GC needs to be used for Class Unloading (above)
:aot [metabase.logger]} ; Log appender class needs to be compiled for log4j to use it
:reflection-warnings {:global-vars {*warn-on-reflection* true}} ; run `lein check-reflection-warnings` to check for reflection warnings
:expectations {:injections [(require 'metabase.test-setup)]
:resource-paths ["test_resources"]
......
log4j.rootLogger=WARN, console
log4j.rootLogger=WARN, console, metabase
# log to the console
log4j.appender.console=org.apache.log4j.ConsoleAppender
......@@ -14,6 +14,8 @@ log4j.appender.file.MaxBackupIndex=2
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d [%t] %-5p%c - %m%n
log4j.appender.metabase=metabase.logger.Appender
# customizations to logging by package
log4j.logger.metabase=INFO
log4j.logger.metabase.driver=DEBUG
......
(ns metabase.api.log
"`/api/log` endpoints."
(:require [compojure.core :refer [GET POST DELETE PUT]]
[metabase.api.common :refer [defendpoint define-routes]]
[metabase.logger :as logger]))
(defendpoint GET "/"
"Logs."
[]
(logger/get-messages))
(define-routes)
......@@ -9,6 +9,7 @@
[email :as email]
[field :as field]
[label :as label]
[log :as log]
[metric :as metric]
[notify :as notify]
[pulse :as pulse]
......@@ -44,6 +45,7 @@
{:status 200 :body {:status "ok"}}
{:status 503 :body {:status "initializing" :progress ((resolve 'metabase.core/initialization-progress))}}))
(context "/label" [] (+auth label/routes))
(context "/log" [] (+auth log/routes))
(context "/metric" [] (+auth metric/routes))
(context "/notify" [] (+apikey notify/routes))
(context "/pulse" [] (+auth pulse/routes))
......
......@@ -16,6 +16,7 @@
[db :as db]
[driver :as driver]
[events :as events]
[logger :as logger]
[metabot :as metabot]
[middleware :as mb-middleware]
[routes :as routes]
......
(ns metabase.logger
(:gen-class
:extends org.apache.log4j.AppenderSkeleton
:name metabase.logger.Appender))
(def messages (atom ()))
(defn get-messages
[]
@messages)
(defn -append [this event]
(swap! messages conj (.getMessage event))
nil)
(defn -close [this]
nil)
(defn -requiresLayout [this]
false)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment