diff --git a/tensorboard/backend/event_processing/data_provider.py b/tensorboard/backend/event_processing/data_provider.py index a391ef1a45..5c29f55701 100644 --- a/tensorboard/backend/event_processing/data_provider.py +++ b/tensorboard/backend/event_processing/data_provider.py @@ -41,6 +41,9 @@ def __init__(self, multiplexer, logdir): self._multiplexer = multiplexer self._logdir = logdir + def __str__(self): + return "MultiplexerDataProvider(logdir=%r)" % self._logdir + def _validate_context(self, ctx): if type(ctx).__name__ != "RequestContext": raise TypeError("ctx must be a RequestContext; got: %r" % (ctx,)) diff --git a/tensorboard/data/grpc_provider.py b/tensorboard/data/grpc_provider.py index f83b60c98e..e84d4350e0 100644 --- a/tensorboard/data/grpc_provider.py +++ b/tensorboard/data/grpc_provider.py @@ -46,6 +46,9 @@ def __init__(self, addr, stub): self._addr = addr self._stub = stub + def __str__(self): + return "GrpcDataProvider(addr=%r)" % self._addr + def data_location(self, ctx, *, experiment_id): req = data_provider_pb2.GetExperimentRequest() req.experiment_id = experiment_id diff --git a/tensorboard/default.py b/tensorboard/default.py index 961688d868..aead4270c6 100644 --- a/tensorboard/default.py +++ b/tensorboard/default.py @@ -71,7 +71,7 @@ class ExperimentalNpmiPlugin( # Ordering matters. The order in which these lines appear determines the # ordering of tabs in TensorBoard's GUI. _PLUGINS = [ - core_plugin.CorePluginLoader, + core_plugin.CorePluginLoader(include_debug_info=True), scalars_plugin.ScalarsPlugin, custom_scalars_plugin.CustomScalarsPlugin, images_plugin.ImagesPlugin, diff --git a/tensorboard/http_api.md b/tensorboard/http_api.md index cbc90b8516..2d29e50648 100644 --- a/tensorboard/http_api.md +++ b/tensorboard/http_api.md @@ -161,12 +161,22 @@ Example response: Returns environment in which the TensorBoard app is running. +The `version` is the Pip version string of the TensorBoard server, like +`"2.4.0a0"`. + +The `window_title` is the value of the `--window_title` flag. + The `data_location` is a user-readable string describing the source from which TensorBoard is reading data, such as a directory on disk. +The response may also include a `debug` field, with information that may be +useful for humans inspecting the system. The contents and structure of `debug` +are subject to change. + Example response: { + "version": "2.4.0", "window_title": "Custom Name", "data_location": "/Users/tbuser/tensorboard_data/" } diff --git a/tensorboard/plugins/core/BUILD b/tensorboard/plugins/core/BUILD index b8a4b7feb9..7ba15fef7d 100644 --- a/tensorboard/plugins/core/BUILD +++ b/tensorboard/plugins/core/BUILD @@ -11,6 +11,7 @@ py_library( srcs_version = "PY3", deps = [ "//tensorboard:plugin_util", + "//tensorboard:version", "//tensorboard/backend:http_util", "//tensorboard/plugins:base_plugin", "//tensorboard/util:grpc_util", diff --git a/tensorboard/plugins/core/core_plugin.py b/tensorboard/plugins/core/core_plugin.py index c43a4636fc..f9770ce9bd 100644 --- a/tensorboard/plugins/core/core_plugin.py +++ b/tensorboard/plugins/core/core_plugin.py @@ -31,6 +31,7 @@ from tensorboard.plugins import base_plugin from tensorboard.util import grpc_util from tensorboard.util import tb_logging +from tensorboard import version logger = tb_logging.get_logger() @@ -50,18 +51,24 @@ class CorePlugin(base_plugin.TBPlugin): plugin_name = "core" - def __init__(self, context): + def __init__(self, context, include_debug_info=None): """Instantiates CorePlugin. Args: context: A base_plugin.TBContext instance. + include_debug_info: If true, `/data/environment` will include some + basic information like the TensorBoard server version. Disabled by + default to prevent surprising information leaks in custom builds of + TensorBoard. """ + self._flags = context.flags logdir_spec = context.flags.logdir_spec if context.flags else "" self._logdir = context.logdir or logdir_spec self._window_title = context.window_title self._path_prefix = context.flags.path_prefix if context.flags else None self._assets_zip_provider = context.assets_zip_provider self._data_provider = context.data_provider + self._include_debug_info = bool(include_debug_info) def is_active(self): return True @@ -165,13 +172,7 @@ def _serve_index(self, index_asset_bytes, request): @wrappers.Request.application def _serve_environment(self, request): - """Serve a JSON object containing some base properties used by the - frontend. - - * data_location is either a path to a directory or an address to a - database (depending on which mode TensorBoard is running in). - * window_title is the title of the TensorBoard web page. - """ + """Serve a JSON object describing the TensorBoard parameters.""" ctx = plugin_util.context(request.environ) experiment = plugin_util.experiment_id(request.environ) data_location = self._data_provider.data_location( @@ -182,6 +183,7 @@ def _serve_environment(self, request): ) environment = { + "version": version.VERSION, "data_location": data_location, "window_title": self._window_title, } @@ -193,12 +195,37 @@ def _serve_environment(self, request): "creation_time": experiment_metadata.creation_time, } ) + if self._include_debug_info: + environment["debug"] = { + "data_provider": str(self._data_provider), + "flags": self._render_flags(), + } return http_util.Respond( request, environment, "application/json", ) + def _render_flags(self): + """Return a JSON-and-human-friendly version of `self._flags`. + + Like `json.loads(json.dumps(self._flags, default=str))` but + without the wasteful serialization overhead. + """ + if self._flags is None: + return None + + def go(x): + if isinstance(x, (type(None), str, int, float)): + return x + if isinstance(x, (list, tuple)): + return [go(v) for v in x] + if isinstance(x, dict): + return {str(k): go(v) for (k, v) in x.items()} + return str(x) + + return go(vars(self._flags)) + @wrappers.Request.application def _serve_logdir(self, request): """Respond with a JSON object containing this TensorBoard's logdir.""" @@ -270,6 +297,9 @@ def _serve_experiment_runs(self, request): class CorePluginLoader(base_plugin.TBLoader): """CorePlugin factory.""" + def __init__(self, include_debug_info=None): + self._include_debug_info = include_debug_info + def define_flags(self, parser): """Adds standard TensorBoard CLI flags to parser.""" parser.add_argument( @@ -640,7 +670,7 @@ def fix_flags(self, flags): def load(self, context): """Creates CorePlugin instance.""" - return CorePlugin(context) + return CorePlugin(context, include_debug_info=self._include_debug_info) def _gzip(bytestring): diff --git a/tensorboard/plugins/core/core_plugin_test.py b/tensorboard/plugins/core/core_plugin_test.py index 269356fb3e..d662c35164 100644 --- a/tensorboard/plugins/core/core_plugin_test.py +++ b/tensorboard/plugins/core/core_plugin_test.py @@ -190,6 +190,27 @@ def testEnvironmentForLogdir(self): parsed_object = self._get_json(self.server, "/data/environment") self.assertEqual(parsed_object["data_location"], self.get_temp_dir()) + def testEnvironmentDebugOffByDefault(self): + parsed_object = self._get_json(self.server, "/data/environment") + self.assertNotIn("debug", parsed_object) + + def testEnvironmentDebugOnExplicitly(self): + multiplexer = event_multiplexer.EventMultiplexer() + logdir = self.get_temp_dir() + provider = data_provider.MultiplexerDataProvider(multiplexer, logdir) + context = base_plugin.TBContext( + assets_zip_provider=get_test_assets_zip_provider(), + logdir=logdir, + data_provider=provider, + window_title="title foo", + ) + plugin = core_plugin.CorePlugin(context, include_debug_info=True) + app = application.TensorBoardWSGI([plugin]) + server = werkzeug_test.Client(app, wrappers.BaseResponse) + + parsed_object = self._get_json(server, "/data/environment") + self.assertIn("debug", parsed_object) + def testLogdir(self): """Test the format of the data/logdir endpoint.""" parsed_object = self._get_json(self.server, "/data/logdir")