diff --git a/.editorconfig b/.editorconfig index 83ff3366d4e2cc77d1184c2837bcec54d68724dc..b2ab82eb6af18ff5e2265f28c409f85d281f565b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,6 +16,7 @@ indent_style = tab [*.clj] indent_size = 2 +max_line_length = 120 [*.css] indent_size = 2 diff --git a/frontend/src/metabase/admin/tasks/containers/Logs.jsx b/frontend/src/metabase/admin/tasks/containers/Logs.jsx index 9277d99d3c44e7670ecc851f15ca3b0bb8656bd9..247779c9ad320b56272c52825dd0e55417c3b006 100644 --- a/frontend/src/metabase/admin/tasks/containers/Logs.jsx +++ b/frontend/src/metabase/admin/tasks/containers/Logs.jsx @@ -9,7 +9,10 @@ import reactAnsiStyle from "react-ansi-style"; import "react-ansi-style/inject-css"; import _ from "underscore"; +import moment from "moment"; +import { t } from "ttag"; +import Select, { Option } from "metabase/components/Select"; import { addCSSRule } from "metabase/lib/dom"; import colors from "metabase/lib/colors"; @@ -27,6 +30,22 @@ const ANSI_COLORS = { for (const [name, color] of Object.entries(ANSI_COLORS)) { addCSSRule(`.react-ansi-style-${name}`, `color: ${color} !important`); } +const MAX_LOGS = 50000; + +function logEventKey(ev) { + return `${ev.timestamp}, ${ev.process_uuid}, ${ev.fqns}, ${ev.msg}`; +} + +function mergeLogs(...logArrays) { + return _.chain(logArrays) + .flatten(true) + .sortBy(ev => ev.msg) + .sortBy(ev => ev.process_uuid) + .sortBy(ev => ev.timestamp) + .uniq(true, logEventKey) + .last(MAX_LOGS) + .value(); +} export default class Logs extends Component { constructor() { @@ -34,6 +53,7 @@ export default class Logs extends Component { this.state = { logs: [], scrollToBottom: true, + selectedProcessUUID: "ALL", }; this._onScroll = () => { @@ -52,7 +72,7 @@ export default class Logs extends Component { async fetchLogs() { const logs = await UtilApi.logs(); - this.setState({ logs: logs.reverse() }); + this.setState({ logs: mergeLogs(this.state.logs, logs.reverse()) }); } componentWillMount() { @@ -80,23 +100,69 @@ export default class Logs extends Component { } render() { - const { logs } = this.state; - return ( - <LoadingAndErrorWrapper loading={!logs || logs.length === 0}> - {() => ( - <div - className="rounded bordered bg-light" - style={{ - fontFamily: '"Lucida Console", Monaco, monospace', - fontSize: "14px", - whiteSpace: "pre-line", - padding: "1em", - }} + const { logs, selectedProcessUUID } = this.state; + const filteredLogs = logs.filter( + ev => + !selectedProcessUUID || + selectedProcessUUID === "ALL" || + ev.process_uuid === selectedProcessUUID, + ); + const processUUIDs = _.uniq( + logs.map(ev => ev.process_uuid).filter(Boolean), + ).sort(); + const renderedLogs = filteredLogs.map(ev => { + const timestamp = moment(ev.timestamp).format(); + const uuid = ev.process_uuid || "---"; + return `[${uuid}] ${timestamp} ${ev.level} ${ev.fqns} ${ev.msg}`; + }); + + let processUUIDSelect = null; + if (processUUIDs.length > 1) { + processUUIDSelect = ( + <div className="pb1"> + <label>{t`Select Metabase process:`}</label> + <Select + defaultValue="ALL" + value={this.state.selectedProcessUUID} + onChange={e => + this.setState({ selectedProcessUUID: e.target.value }) + } + className="inline-block ml1" + width={400} > - {reactAnsiStyle(React, logs.join("\n"))} - </div> - )} - </LoadingAndErrorWrapper> + <Option value="ALL" key="ALL">{t`All Metabase processes`}</Option> + {processUUIDs.map(uuid => ( + <Option key={uuid} value={uuid}> + <code>{uuid}</code> + </Option> + ))} + </Select> + </div> + ); + } + + return ( + <div> + {processUUIDSelect} + + <LoadingAndErrorWrapper + loading={!filteredLogs || filteredLogs.length === 0} + > + {() => ( + <div + className="rounded bordered bg-light" + style={{ + fontFamily: '"Lucida Console", Monaco, monospace', + fontSize: "14px", + whiteSpace: "pre-line", + padding: "1em", + }} + > + {reactAnsiStyle(React, renderedLogs.join("\n"))} + </div> + )} + </LoadingAndErrorWrapper> + </div> ); } } diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx index 8c25a4cea80ea23f0abcd6efc86287b2772c0ef0..6cd29e5a1df7d627b47673d9d1c3253fccf16330 100644 --- a/frontend/src/metabase/components/Select.jsx +++ b/frontend/src/metabase/components/Select.jsx @@ -97,7 +97,7 @@ class BrowserSelect extends Component { multiple, } = this.props; - let children = this.props.children; + let children = _.flatten(this.props.children); let selectedNames = children .filter(child => this.isSelected(child.props.value)) diff --git a/src/metabase/config.clj b/src/metabase/config.clj index 79680006622fc364d49ad3901d882487f1ac3774..09f33eff7e9a8644504e933052dccc22ac54b912 100644 --- a/src/metabase/config.clj +++ b/src/metabase/config.clj @@ -5,7 +5,8 @@ [clojure.string :as str] [environ.core :as environ] [metabase.plugins.classloader :as classloader]) - (:import clojure.lang.Keyword)) + (:import clojure.lang.Keyword + java.util.UUID)) (def ^Boolean is-windows? "Are we running on a Windows machine?" @@ -101,6 +102,12 @@ Looks something like `Metabase v0.25.0.RC1`." (str "Metabase " (mb-version-info :tag))) +(defonce ^{:doc "This UUID is randomly-generated upon launch and used to identify this specific Metabase instance during + this specifc run. Restarting the server will change this UUID, and each server in a horizontal cluster + will have its own ID, making this different from the `site-uuid` Setting."} + local-process-uuid + (str (UUID/randomUUID))) + ;; This only affects dev: ;; diff --git a/src/metabase/logger.clj b/src/metabase/logger.clj index 70cb740d4f3dcf61fa112c84c5e35b1be4ea2aa7..8accfc1d0658bb957e10c5cfae893d803b677c2a 100644 --- a/src/metabase/logger.clj +++ b/src/metabase/logger.clj @@ -2,9 +2,8 @@ (:require [amalloy.ring-buffer :refer [ring-buffer]] [clj-time [coerce :as coerce] - [core :as t] [format :as time]] - [clojure.string :as str]) + [metabase.config :refer [local-process-uuid]]) (:import [org.apache.log4j Appender AppenderSkeleton Logger] org.apache.log4j.spi.LoggingEvent)) @@ -17,26 +16,19 @@ [] (reverse (seq @messages*))) -(defonce ^:private formatter (time/formatter "MMM dd HH:mm:ss" (t/default-time-zone))) - -(defn- event->log-string [^LoggingEvent event] - ;; for messages that include an Exception, include the string representation of it (i.e., its stacktrace) - ;; separated by newlines - (str/join - "\n" - (cons - (let [ts (time/unparse formatter (coerce/from-long (.getTimeStamp event))) - level (.getLevel event) - fqns (.getLoggerName event) - msg (.getMessage event)] - (format "%s \033[1m%s %s\033[0m :: %s" ts level fqns msg)) - (seq (.getThrowableStrRep event))))) - +(defn- event->log-data [^LoggingEvent event] + {:timestamp (time/unparse (time/formatter :date-time) + (coerce/from-long (.getTimeStamp event))) + :level (.getLevel event) + :fqns (.getLoggerName event) + :msg (.getMessage event) + :exception (.getThrowableStrRep event) + :process_uuid local-process-uuid}) (defn- metabase-appender ^Appender [] (proxy [AppenderSkeleton] [] (append [event] - (swap! messages* conj (event->log-string event)) + (swap! messages* conj (event->log-data event)) nil) (close [] nil) diff --git a/src/metabase/metabot/instance.clj b/src/metabase/metabot/instance.clj index 0b802a4e9de208f459a57b7ca017e307a5eaa486..57bf42fa8367b98237819a756f02d63f5c46ebd8 100644 --- a/src/metabase/metabot/instance.clj +++ b/src/metabase/metabot/instance.clj @@ -14,26 +14,22 @@ How do we uniquiely identify each instance? - `local-process-uuid` is randomly-generated upon launch and used to identify this specific Metabase instance during - this specifc run. Restarting the server will change this UUID, and each server in a hortizontal cluster will have - its own ID, making this different from the `site-uuid` Setting. The local process UUID is used to differentiate - different horizontally clustered MB instances so we can determine which of them will handle MetaBot duties. - - TODO - if we ever want to use this elsewhere, we need to move it to `metabase.config` or somewhere else central like - that." + `metabase.public-settings/local-process-uuid` is randomly-generated upon launch and used to identify this specific + Metabase instance during this specifc run. Restarting the server will change this UUID, and each server in a + hortizontal cluster will have its own ID, making this different from the `site-uuid` Setting. The local process UUID + is used to differentiate different horizontally clustered MB instances so we can determine which of them will handle + MetaBot duties." (:require [clojure.tools.logging :as log] [honeysql.core :as hsql] + [metabase + [config :refer [local-process-uuid]] + [util :as u]] [metabase.models.setting :as setting :refer [defsetting]] - [metabase.util :as u] [metabase.util [date :as du] [i18n :refer [trs]]] [toucan.db :as db]) - (:import java.sql.Timestamp - java.util.UUID)) - -(defonce ^:private local-process-uuid - (str (UUID/randomUUID))) + (:import java.sql.Timestamp)) (defsetting ^:private metabot-instance-uuid "UUID of the active MetaBot instance (the Metabase process currently handling MetaBot duties.)" diff --git a/src/metabase/pulse/render/table.clj b/src/metabase/pulse/render/table.clj index 99ab03aa10974c7c8023d01f80bfba03a85ca511..aa33bf6c036262a27465b4ef86488957f3849396 100644 --- a/src/metabase/pulse/render/table.clj +++ b/src/metabase/pulse/render/table.clj @@ -4,8 +4,12 @@ [metabase.pulse.render [color :as color] [style :as style]]) - (:import jdk.nashorn.api.scripting.JSObject - metabase.pulse.render.common.NumericWrapper)) + (:import jdk.nashorn.api.scripting.JSObject)) + +;; Our 'helpful' NS declaration linter will complain that common is unused. But we need to require it so +;; NumericWrapper exists in the first place. +(require 'metabase.pulse.render.common) +(import 'metabase.pulse.render.common.NumericWrapper) (defn- bar-th-style [] (merge (style/font-style) {:font-size :14.22px