Giter Club home page Giter Club logo

wobserver's Introduction

Wobserver

Hex.pm Build Status Coverage Status Inline docs Deps Status Hex.pm

Web based metrics, monitoring, and observer.

We are talking about :wobserver at ElixirConf 2017. Check out the presentation and samples and our other talk about Task Bunny.

Click to see more images.

Click to view images

Functionality:

  • Drop-in monitoring though web interface.
  • Metrics endpoint (/metrics) for system monitoring. (Default: Prometheus)
  • Monitoring automation through JSON API.
  • Node management and discovery behind firewalls and load balancers.
  • Easy to extend:
    • Add custom metrics and pages for your project, just by adding them in the config.
    • Just 3 lines of code to add pages/metrics for your library, when users have :wobserver installed. (See how.)

Table of contents

Installation

Hex

Add Wobserver as a dependency to your mix.exs file:

def deps do
  [{:wobserver, "~> 0.1"}]
end

and add it to your list of applications:

def application do
  [applications: [:wobserver]]
end

Then run mix deps.get in your shell to fetch the dependencies.

Note: Check out plug mode to integrate with a Phoenix or other web application. (Prevents startup of separate web server.)

Build manually

Run the following commands to build the project:

$ npm install
$ mix deps.get
$ mix build

Note: Use the package generated by mix build if you want to include the local wobserver in your application. (Unpack in your deps.)

Github

Wobserver does not support being included directly from github. The required assets are not included in the repo in build form and can therefore not be used. It is possible to build locally and use the generated package. (See Build manually for more information.)

Usage

Browser

To view the web interface just enter http://<host>[:<port>]/ in the browser and it should show the :wobserver interface. The default port is 4001, but the port can be changed in the configuration.

A sample interface can be viewed here.

API

The API can be accessed by calling http://<host>[:<port>]/api/. The index will return 404, but specific endpoints should return results.

Remote nodes

The API provides a list of remote nodes by calling http://<host>[:<port>]/api/nodes.

The API of remote nodes can be accessed by calling the API endpoint and prefixing the node name, host, or host:port.

For example considering the following node list:

[
  {
    "port": 4001,
    "name": "node_prime",
    "local?": true,
    "host": "192.168.5.55"
  },
  {
    "port": 80,
    "name": "remote",
    "local?": false,
    "host": "80.23.1.165"
  }
]

The following calls would all work for the first node: (local is a reserved name that always points to the local node.)

http://<host>[:<port>]/api/local/system
http://<host>[:<port>]/api/node_prime/system
http://<host>[:<port>]/api/192.168.5.55/system
http://<host>[:<port>]/api/192.168.5.55:4001/system

And these calls would work for the second node:

http://<host>[:<port>]/api/remote/system
http://<host>[:<port>]/api/80.23.1.165/system
http://<host>[:<port>]/api/80.23.1.165:80/system

System

The API provides a list of system information by calling http://<host>[:<port>]/api/system.

The scheduler is a list of load values (0-1) for each scheduler.

Example:

{
  "statistics": {
    "uptime": 459876,
    "process_total": 122,
    "process_running": 0,
    "process_max": 262144,
    "output": 1259201,
    "input": 12945380
  },
  "scheduler": [
    0.0037370416873916392,
    0.0003088661849770247,
    0.0003072993680801981,
    0.00030274231847091137,
    0.0004706952361156354,
    0.00028556537348788645,
    0.00025471141618606366,
    0.0002522242536713918
  ],
  "memory": {
    "total": 30275576,
    "process": 5242800,
    "ets": 886544,
    "code": 13635797,
    "binary": 288744,
    "atom": 594561
  },
  "cpu": {
    "schedulers_online": 8,
    "schedulers_available": 8,
    "schedulers": 8,
    "logical_processors_online": 8,
    "logical_processors_available": "unknown",
    "logical_processors": 8
  },
  "architecture": {
    "wordsize_internal": 8,
    "wordsize_external": 8,
    "threads": true,
    "thread_pool_size": 10,
    "system_architecture": "x86_64-apple-darwin15.6.0",
    "smp_support": true,
    "otp_release": "19",
    "kernel_poll": false,
    "erts_version": "8.2",
    "elixir_version": "1.4.0"
  }
}

Allocators

The API provides a list of allocators and their size by calling http://<host>[:<port>]/api/allocators.

Example:

[
  {
    "type": "sl_alloc",
    "carrier": 294912,
    "block": 664
  },
  {
    "type": "std_alloc",
    "carrier": 1081344,
    "block": 498184
  },
  {
    "type": "ll_alloc",
    "carrier": 35913728,
    "block": 26080144
  },
  {
    "type": "eheap_alloc",
    "carrier": 9830400,
    "block": 2634720
  },
  {
    "type": "ets_alloc",
    "carrier": 3178496,
    "block": 890880
  },
  ...
]

Application

The API provides a list of applications and their descriptions by calling http://<host>[:<port>]/api/application.

The information for a specific application, including the process hierarchy can be found by calling http://<host>[:<port>]/api/application/<application-name>.

Example: http://localhost:4001/api/application

[
  {
    "version": "0.1.0",
    "name": "wobserver",
    "description": "Web based metrics, monitoring, and observer."
  },
  {
    "version": "1.3.0",
    "name": "plug",
    "description": "A specification and conveniences for composable modules between web applications"
  },
  {
    "version": "1.1.0",
    "name": "cowboy",
    "description": "Small, fast, modular HTTP server."
  },
  {
    "version": "1.2.1",
    "name": "ranch",
    "description": "Socket acceptor pool for TCP protocols."
  },
  {
    "version": "0.6.1",
    "name": "credo",
    "description": "A static code analysis tool for the Elixir language with a focus on code consistency and teaching."
  },
  {
    "version": "0.2.0",
    "name": "bunt",
    "description": "256 color ANSI coloring in the terminal"
  },
  {
    "version": "1.6.5",
    "name": "hackney",
    "description": "simple HTTP client"
  },
  {
    "version": "1.4.0",
    "name": "logger",
    "description": "logger"
  },
  ...
]

http://localhost:4001/api/application/elixir

{
  "pid": "#PID<0.59.0>",
  "name": "<0.59.0>",
  "meta": {
    "status": "waiting",
    "init": "proc_lib.init_p/5",
    "current": "application_master.main_loop/2",
    "class": "application"
  },
  "children": [
    {
      "pid": "#PID<0.60.0>",
      "name": "<0.60.0>",
      "meta": {
        "status": "waiting",
        "init": "application_master.start_it/4",
        "current": "application_master.loop_it/4",
        "class": "unknown"
      },
      "children": [
          {
            "pid": "#PID<0.61.0>",
            "name": "elixir_sup",
            "meta": {
              "status": "waiting",
              "init": "proc_lib.init_p/5",
              "current": "gen_server.loop/6",
              "class": "supervisor"
            },
            "children": [
              {
              "pid": "#PID<0.62.0>",
              "name": "elixir_config",
              "meta": {
                "status": "waiting",
                "init": "proc_lib.init_p/5",
                "current": "gen_server.loop/6",
                "class": "gen_server"
              },
              "children": []
            },
            {
              "pid": "#PID<0.63.0>",
              "name": "elixir_code_server",
              "meta": {
                "status": "waiting",
                "init": "proc_lib.init_p/5",
                "current": "gen_server.loop/6",
                "class": "gen_server"
              },
              "children": []
            }
          ]
        }
      ]
    }
  ]
}

Process

The API provides a list of processes and their basic information by calling http://<host>[:<port>]/api/process.

The information for a specific process, including a links, memory usage, and state can be found by calling http://<host>[:<port>]/api/application/<process-name>.

The process name can be given as pid, name, or short pid.

So all the following are valid:

http://localhost:4001/api/process/<0.247.0>
http://localhost:4001/api/process/#PID<0.247.0>   # Rememeber to url encode # -> %23
http://localhost:4001/api/process/Wobserver.Supervisor

Example: http://localhost:4001/api/process

{
  "processes": [
    {
      "reductions": 162714,
      "pid": "#PID<0.247.0>",
      "nr1": "0",
      "message_queue_length": 0,
      "memory": 11888,
      "init": "timer_server",
      "current": "gen_server.loop/6"
    },
    {
      "reductions": 95,
      "pid": "#PID<0.243.0>",
      "nr1": "0",
      "message_queue_length": 0,
      "memory": 2792,
      "init": "erlang.apply/2",
      "current": "io.execute_request/2"
    },
    {
      "reductions": 954,
      "pid": "#PID<0.242.0>",
      "nr1": "0",
      "message_queue_length": 0,
      "memory": 16808,
      "init": "Elixir.IEx.Evaluator.init/4",
      "current": "Elixir.IEx.Evaluator.loop/3"
    },
    ...
  ]
}

http://localhost:4001/api/process/<0.247.0>

{
  "trap_exit": true,
  "state": "[]",
  "relations": {
    "links": [
      "#PID<0.53.0>"
    ],
    "group_leader": "#PID<0.33.0>",
    "ancestors": [
      "kernel_safe_sup",
      "kernel_sup",
      "#PID<0.34.0>"
    ]
  },
  "registered_name": "timer_server",
  "priority": "normal",
  "pid": "#PID<0.247.0>",
  "meta": {
    "status": "waiting",
    "init": "proc_lib.init_p/5",
    "current": "gen_server.loop/6",
    "class": "gen_server"
  },
  "message_queue_len": 0,
  "memory": {
    "total": 0,
    "stack_size": 9,
    "stack_and_heap": 1974,
    "heap_size": 1598,
    "gc_min_heap_size": 233,
    "gc_full_sweep_after": 65535
  },
  "error_handler": "error_handler"
}

Ports

The API provides a list of ports and their owners by calling http://<host>[:<port>]/api/ports.

Example: http://localhost:4001/api/ports

[
  {
    "output": 0,
    "os_pid": "undefined",
    "name": "forker",
    "links": [],
    "input": 0,
    "id": 0,
    "connected": "#PID<0.0.0>"
  },
  {
    "output": 3,
    "os_pid": "undefined",
    "name": "efile",
    "links": [
      "#PID<0.4.0>"
    ],
    "input": 46,
    "id": 8,
    "connected": "#PID<0.4.0>"
  },
  {
    "output": 18810,
    "os_pid": "undefined",
    "name": "efile",
    "links": [
      "#PID<0.44.0>"
    ],
    "input": 23874,
    "id": 4680,
    "connected": "#PID<0.44.0>"
  },
  ...
]

Table view

The API provides a list of tables and their details by calling http://<host>[:<port>]/api/table.

The information for a specific details, including a the actual data can be found by calling http://<host>[:<port>]/api/table/<table-name>.

Example: http://localhost:4001/api/table Example:

[
  {
    "type": "set",
    "size": 0,
    "protection": "protected",
    "owner": "#PID<0.247.0>",
    "name": "timer_interval_tab",
    "meta": {
      "write_concurrency": false,
      "read_concurrency": false,
      "compressed": false
    },
    "memory": 304,
    "id": "timer_interval_tab"
  },
  {
    "type": "ordered_set",
    "size": 7,
    "protection": "protected",
    "owner": "#PID<0.247.0>",
    "name": "timer_tab",
    "meta": {
      "write_concurrency": false,
      "read_concurrency": false,
      "compressed": false
    },
    "memory": 304,
    "id": "timer_tab"
  },
  {
    "type": "set",
    "size": 7,
    "protection": "public",
    "owner": "#PID<0.228.0>",
    "name": "workstore",
    "meta": {
      "write_concurrency": false,
      "read_concurrency": false,
      "compressed": false
    },
    "memory": 5138,
    "id": 417840
  },
  ...
]

http://localhost:4001/api/table/timer_interval_tab

{
  "type": "set",
  "size": 0,
  "protection": "protected",
  "owner": "#PID<0.247.0>",
  "name": "timer_interval_tab",
  "meta": {
    "write_concurrency": false,
    "read_concurrency": false,
    "compressed": false
  },
  "memory": 304,
  "id": "timer_interval_tab",
  "data": []
}

Metrics

Metrics are available by calling http://<host>[:<port>]/metrics.

The metrics are by default formatted for Prometheus, but can be configured to work with any system. An explanation of how to configure the metrics format and how to add metrics to the output will be added later.

http://localhost:4001/metrics

# HELP erlang_vm_used_memory_bytes Memory usage of the Erlang VM.
# TYPE erlang_vm_used_memory_bytes gauge
erlang_vm_used_memory_bytes{node="10.74.181.35",type="atom"} 553593
erlang_vm_used_memory_bytes{node="10.74.181.35",type="binary"} 359552
erlang_vm_used_memory_bytes{node="10.74.181.35",type="code"} 13533686
erlang_vm_used_memory_bytes{node="10.74.181.35",type="ets"} 1899472
erlang_vm_used_memory_bytes{node="10.74.181.35",type="process"} 6048552
# HELP erlang_vm_used_io_bytes IO counter for the Erlang VM.
# TYPE erlang_vm_used_io_bytes counter
erlang_vm_used_io_bytes{node="10.74.181.35",type="input"} 11301316
erlang_vm_used_io_bytes{node="10.74.181.35",type="output"} 618157

Configuration

Port

The port can be set in the config by setting :port for :wobserver to a valid number.

Example config

config :wobserver,
  port: 80

Mode

Wobserver runs by default in :standalone mode. This means that :wobserver will start its own :cowboy listeners on a separate port. Standalone mode is ideal for drop-in web viewing, but might not be ideal if another part of the application is already running an web server. It is possible to enable :plug mode to prevent :wobserver from starting :cowboy to handle web requests.

Standalone

Standalone mode is the default operating mode. A :cowboy (ranch) listener will be started with 10 accepters and a websocket. Set mode to :standalone in the :wobserver configuration to force standalone mode.

Example:

config :wobserver,
  mode: :standalone

Plug

Plug mode prevents :wobserver from starting :cowboy (ranch). Set mode to :plug in the :wobserver configuration to use plug mode. Set remote_url_prefix to the url prefix you put :wobserver behind to make sure dns node discovery still functions.

cowboy

Plug mode prevents :wobserver from starting :cowboy (ranch). Set mode to :plug in the :wobserver configuration to use plug mode. Set remote_url_prefix to the url prefix you put :wobserver behind to make sure dns node discovery still functions.

Add the following line of code to the application's router to forward requests to :wobserver:

  forward "/wobserver", to: Wobserver.Web.Router

Add the following option to the :cowboy child_spec to enable use of the :wobserver websocket:

dispatch: [
    {:_, [
      {"/ws", Wobserver.Web.Client, []},
      {:_, Cowboy.Handler, {<your own router>, []}}
    ]}
  ],
Phoenix

Add the following line of code to the Phoenix router to forward requests to :wobserver:

  forward "/wobserver", Wobserver.Web.Router

Add the following option to your Phoenix applications Endpoint to enable use of the :wobserver websocket (the path should match what is in the 'forward' in your router):

  socket "/wobserver", Wobserver.Web.PhoenixSocket
Cowboy Example

config.exs

config :wobserver,
  mode: :plug,
  remote_url_prefix: "/wobserver"

my_router.ex

defmodule MyApp.MyRouter do
  use Plug.Router

  plug :match
  plug :dispatch

  forward "/wobserver", to: Wobserver.Web.Router
end

application.ex

defmodule MyApp.Application do
  use Application

  alias Plug.Adapters.Cowboy

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    options = [
      dispatch: [
        {:_, [
          {"/wobserver/ws", Wobserver.Web.Client, []},
          {:_, Cowboy.Handler, {MyApp.MyRouter, []}}
        ]}
      ],
    ]

    children = [
      Cowboy.child_spec(:http, MyApp.MyRouter, [], options)
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Node Discovery

The method used can be set in the config file by setting:

config :wobserver,
  discovery: :none

The following methods can be used: (default: :none)

  • :none, just returns the local node.
  • :dns, use DNS to search for other nodes. The option discovery_search needs to be set to filter entries.
  • :custom, a function as String.

Example config

No discovery: (default)

config :wobserver,
  port: 80,
  discovery: :none

Using dns as discovery service:

config :wobserver,
  port: 80,
  discovery: :dns,
  discovery_search: "google.nl"

Using a custom function:

config :wobserver,
  port: 80,
  discovery: :custom,
  discovery_search: &MyApp.CustomDiscovery.discover/0

Using an anonymous function:

config :wobserver,
  port: 80,
  discovery: :custom,
  discovery_search: fn -> [] end

Both the custom and anonymous functions can be given as a String, which will get evaluated.

Metrics

Add Metrics

Config

Metrics and metric generators can be added by setting them in the configuration.

To add custom metrics set the :metrics option. The :metrics option must be a keyword list with the following keys:

  • additional, for a keyword list with additional metrics.
  • generators, for a list of metric generators.

The following settings are accepted for additional:

  • keyword list, the key is the name of the metric and the value is the metric data.

The following inputs are accepted for metric generators:

  • list of callable functions. Every function should return a keyword list with as key the name of the metric and as value the metric data.

For more information about how to format metric data see: Wobserver.Util.Metrics.Formatter.format_all/1.

For example this configuration:

config :wobserver,
  metrics: [
    additional: [
      example: {fn -> [red: 5] end, :gauge, "Description"},
    ],
    generators: [
      "&MyApp.generator/0",
      fn -> [bottles: {fn -> [wall: 8, floor: 10] end, :gauge, "Description"}] end
      fn -> [server: {"MyApp.Server.metrics/0", :gauge, "Description"}] end
    ]
  ]

Dynamically

Metrics and metric generators can also be added dynamically at runtime.

To register a metric you need to pass a keyword list to Wobserver.register with the same data as you would set in the configuration file.

For example:

Wobserver.register :metric, [example: {fn -> [red: 5] end, :gauge, "Description"}]

To register a metric generator you need to pass a list of functions to Wobserver.register.

For example:

Wobserver.register :metric, [&MyLibrary.Metrics.generate/0]

Formatting

A custom formatter can be created for output of metrics by implementing the Wobserver.Util.Metrics.Formatter behavior. This custom formatter can be enabled in the configuration file by setting metric_format.

For example this configuration:

config :wobserver,
  metric_format: JsonFormatter

And this simple JSON formatter:

defmodule SimpleJsonFormatter do
  @behaviour Wobserver.Util.Metrics.Formatter

  def format_data(name, data, type, help) do
    formatted_data =
      data
      |> Enum.map(fn {value, labels} ->
           %{value: value, labels: Enum.into(labels, %{})}
         end)

    %{
      name: name,
      type: type,
      description: help,
      data: formatted_data
    }
    |> Poison.encode!
  end

  def combine_metrics(metrics) do
    "[" <> Enum.join(metrics,",") <> "]"
  end

  def merge_metrics(metrics) do
    "[" <> Enum.join(metrics,",") <> "]"
  end
end

Produce the following output:

[
  [
    {
      "type": "gauge",
      "name": "erlang_vm_used_memory_bytes",
      "description": "Memory usage of the Erlang VM.",
      "data": [
        {
          "value": 654241,
          "labels": {
            "type": "atom",
            "node": "192.168.1.88"
          }
        },
          {
          "value": 503464,
          "labels": {
            "type": "binary",
            "node": "192.168.1.88"
          }
        },
        {
          "value": 14459399,
          "labels": {
            "type": "code",
            "node": "192.168.1.88"
          }
        },
        {
          "value": 2073072,
          "labels": {
            "type": "ets",
            "node": "192.168.1.88"
          }
        },
        {
          "value": 6008488,
          "labels": {
            "type": "process",
            "node": "192.168.1.88"
          }
        }
      ]
    },
    {
      "type": "counter",
      "name": "erlang_vm_used_io_bytes",
      "description": "IO counter for the Erlang VM.",
      "data": [
        {
          "value": 29523254,
          "labels": {
            "type": "input",
            "node": "192.168.1.88"
          }
        },
        {
          "value": 9960593,
          "labels": {
            "type": "output",
            "node": "192.168.1.88"
          }
        }
      ]
    }
  ]
]

Pages

Pages are custom views in the web interface and endpoints in the JSON API for an application or library.

There are two ways to add a custom page:

  • config, set a list of custom pages in the mix config.
  • registration, call Wobserver.register/2 and dynamically add pages.

Config

Adding more pages to :wobserver can be done by setting the :pages option.

The :pages option must be a list of page data.

The page data can be formatted as:

  • {title, command, callback}
  • {title, command, callback, options}
  • a map with the following fields:
    • title
    • command
    • callback
    • options (optional)

For more information and types see: Wobserver.Page.register/1.

Example:

config :wobserver,
  pages: [
    {"Example", :example, fn -> %{x:  9} end}
  ]

Dynamically

Dynamically register a page with :wobserver by calling Wobserver.register/2.

The following inputs are accepted:

  • {title, command, callback}
  • {title, command, callback, options}
  • a map with the following fields:
    • title
    • command
    • callback
    • options (optional)

The fields are used as followed:

  • title, the name of the page. Is used for the web interface menu.
  • command, single atom to associate the page with.
  • callback, function to be evaluated, when the a api is called or page is viewd. The result is converted to JSON and displayed.
  • options, options for the page.

The following options can be set:

  • api_only (boolean), if set to true the page won't show up in the web interface, but will only be available as API.
  • refresh (float, 0-1), sets the refresh time factor. Used in the web interface to refresh the data on the page. Set to 0 for no refresh.

Example:

Wobserver.register(:page, {"My App", :my_app, fn -> %{data: 123} end})

Library Integration

Integrating a library with :wobserver is done by calling Wobserver.register/2, when the library loads, and dynamically adding pages and metrics.

Code

To safely integrate with :wobserver use the following code:

if Code.ensure_loaded(Wobserver) == {:module, Wobserver} do
  Wobserver.register :page, {"My Library", :my_library, fn -> %{data: 123} end}
  Wobserver.register :metric, [&MyLibrary.Metrics.generate/0]
end

The above code will make sure that the library only calls register, when :wobserver is loaded. This will prevent the library from trying to register, when :wobserver is not installed.

For an implementation see the :task_bunny library: TaskBunny, lib/task_bunny.ex.

Remove Warnings

The above code will generate warnings while compiling the library.

warning: function Wobserver.register/2 is undefined (module Wobserver is not available)

There are two options to remove those warnings.

Edit mix.exs

The mix.exs file can be edited to excluded Wobserver from reference checks.

To do this add the following line to project/0 in your mix file:

  xref: [exclude: [Wobserver]]

Kernel.apply

The code can be rewritten to use Kernel.apply/3. The following code will be less readable and slightly slower, but will not generate warnings.

if Code.ensure_loaded(Wobserver) == {:module, Wobserver} do
  apply Wobserver, :register, [:page, {"My Library", :my_library, fn -> %{data: 123} end}]
  apply Wobserver, :register, [:metric, [&MyLibrary.Metrics.generate/0]]
end

Improvements

  • Cleanup namespaces.
  • Cleanup readme, condense sample output.
  • Overhaul web interface (make fancier/pleasant)

Contributors

  • OvermindDL1 - Phoenix Socket support and lots of issue reports.

License

Wobserver source code is released under the MIT License. Check LICENSE file for more information.

wobserver's People

Contributors

ianluites avatar overminddl1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wobserver's Issues

Monitoring remote node

Can we run it stand-alone and monitor other node (if that node doesn't have wobserver installed)?

How to protect "/wobserver" (plug mode)?

Hello,

I want to protect everything below /wobserver from unauthorized users. So far I have tried

  • basic auth
  • cookie based authorization with a plug

In both cases I hit the same dead end: the required cookie/header was not being sent at all below /wobserver/api. I suspect this is an idiosyncrasy of the javascript "fetch" API that is being used.

I found references to an undocumented security feature which, as I understand it, is needed for inter-node communication (and I only have one node for the forseeable future). Will this feature also authorize users? Otherwise, is there a workaround for the problem with cookies and certain headers not being sent?

websocket connection fails over SSL.

Locally this works without issue. When hosted using SSL, I have Phoenix forcing SSL, and it seems to break the websocket connection.

I get "Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS."

I'm using Phoenix channels, and my application doesn't have any socket issues on other parts. Maybe I'm doing something wrong?

cannot copy-paste from pages

We display operational data that would be handy to copy and move around. Right now, we can't select anything to copy it.

Conflicting cowboy versions with plug_cowboy

Could you please update the cowboy version?

Failed to use "cowboy" (version 2.8.0) because
  plug_cowboy (version 2.4.1) requires ~> 2.7
  wobserver (version 0.1.8) requires ~> 1.1

Make Wobserver usable as a standalone Plug

Instead of having wobserver run its own cowboy instance with (apparently) no authentication, I'd like to be able to run wobserver from inside an app's existing administrator panel.

The main barrier to this is wobserver's websockets, which can be replaced with EventSource.

CSRF conflict in Plug/Phoenix configuration

I had put wobserver behind a protected pipeline that included plug :protect_from_forgery (a wrapper around plug Plug.CSRFProtection) and could not get wobserver working at all (Safari would refuse to load the app.js for wobserver and the log would report a CSRF protection violation for cross-origin resources).

As soon as I removed :protect_from_forgery from the pipeline, wobserver worked just fine. It would be great to have documentation on how to enable CSRF protection, but at a minimum, users should be warned that wobserver is not currently compatible with Plug.CSRFProtection and :protect_from_forgery.

`mix build` is too top-level

Custom mix tasks by Elixir suggestions should be prefixed with the project name, so it should be mix wobserver.build. A simple change and figured I should suggest it before the project gets too far along. :-)

The reason is though that if multiple libraries include mix tasks, because your task 'is' imported into the projects that depend on this library, then they can conflict or become uncallable or even override what is expected from another library.

If you are wanting to define a new mix task that is not imported into the users project that includes this library then you should use a mix alias instead. An example (just add this to the bottom of your mix.exs file while adding a aliases: aliases() to your project list):

  defp aliases do
    [
      build: &WobServer.Builder.build/1,
    ]
  end

This example would add a new task of the name build that is exposed only to this project (and not projects that depend on this project), this task will call the WobServer.Builder.build/1 function, just like your task gets called in its run/1. :-)

Aliases have a lot of other features (see above link), including calling tasks before or after others, you can, say, override 'compile' so it always calls 'build' first and then the normal 'compile' (that way you do not need to distribute your Assets file but instead it can be built on-the-fly when the dependency is compiled, I'm 'pretty' sure dependency building calls the compile alias in mix, but uncertain, would want to check...).

:-)

Connection lost trouble shooting

I am trying to use the project, I have a server running phoenix and wobserver.
I have added a list of nodes I would like to connect but I keep getting the "Connection lost" error.
The two machine are in the same subnetwork without any firewall.

I am looking for tips, but also it would be nice to have a little more infos either on the logs or in the webpage on what's going on.

Thanks

Port list/info sometimes crashes application

The &Wobserver.Port.info/1 sometimes crashes the application by having an :undefined port.

Fix by adding a case for :undefined and filtering those from the resulting list in &Wobserver.Port.list/0.

:sys.get_status/2 Causes exception in Ranch

I am seeing the following error show up in the logs:

Ranch acceptor received unexpected message: {:system, {#PID<0.982.0>, #Reference<0.736632836.3709599745.155908>},
 :get_status}

Which is logged from this line:
https://github.com/ninenines/ranch/blob/1.3.2/src/ranch_acceptor.erl#L53

It seems to be coming from this line
https://github.com/shinyscorpion/wobserver/blob/master/lib/wobserver/util/process.ex#L224

Searching my dependencies it looks like there are only 2 calls to :sys.get_status/2, one from within ranch itself and another from this library.

HTTPS not supported in the wobserver_api_fallback

When websockets don't work or respond, the js falls back to the api instead of the websockets. But the wobserver_api_fallback forces a url with an http scheme, which causes insecure request errors when the application is using https.

No authorization test

Currently we can put a plug in front of the router for wobserver to block access for an unauthorized user, but currently there is no way to do the same for the socket without hijacking between them. The router should probably take a callback function that we can supply that gets passed the plug's conn and returns either a token (binary? easily generate-able securely via phoenix for example by the user) or an error. If a token is returned then they are allowed to access the wobserver, if an error then it should just 404 or whatever. The token should also be passed to the socket via the web page (or cookie) and the socket should verify that it is an allowed token (perhaps by passing it back to yet another user function, these could be specified in the config or on the injection sites), and if it is verified and allowed then the socket connection establishes else it is dropped.

Currently even if blocking the /wobserver route the outside can still access the websocket directly to query.

The most simple solution would probably just be adding 2 callback functions to the config, and if they exist use them, else allow all as it does now.

:badarith error

In the logs I keep getting a :badarith error randomly between 2 to 30 seconds, the stacktrace is (reformatted for readability):

** (ErlangError) erlang error:
[
  reason: :badarith,
  mfa: {Wobserver.Web.Client, :websocket_handle, 3},
  stacktrace: [
    {Wobserver.System.Scheduler, :"-utilization/0-fun-0-", 1, [file: 'lib/wobserver/system/scheduler.ex', line: 37]},
    {Enum, :"-map/2-lists^map/1-0-", 2, [file: 'lib/enum.ex', line: 1229]},
    {Wobserver.System, :overview, 0, [file: 'lib/wobserver/system.ex', line: 54]},
    {Wobserver.Web.Client, :client_handle, 2, [file: 'lib/wobserver/web/client.ex', line: 47]},
    {Wobserver.Web.Client, :websocket_handle, 3, [file: 'lib/wobserver/web/client.ex', line: 5]},
    {:cowboy_websocket, :handler_call, 7, [file: 'c:/Users/<user>/Projects/my_server/deps/cowboy/src/cowboy_websocket.erl', line: 588]},
    {:cowboy_protocol, :execute, 4, [file: 'c:/Users/<user>/Projects/my_server/deps/cowboy/src/cowboy_protocol.erl', line: 442]}
  ],
  msg: {:text, "{\"command\":\"system\",\"data\":null}"},
  req: [
    socket: #Port<0.89433>,
    transport: :ranch_tcp,
    connection: :keepalive,
    pid: #PID<0.1515.0>,
    method: "GET",
    version: :"HTTP/1.1",
    peer: {{10, 1, 2, 158}, 64429},
    host: "<user>",
    host_info: :undefined,
    port: 80,
    path: "/observer/ws",
    path_info: :undefined,
    qs: "",
    qs_vals: :undefined,
    bindings: [],
    headers: [
      {"host", "<user>"},
      {"user-agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0"},
      {"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
      {"accept-language", "en-US,en;q=0.5"}, {"accept-encoding", "gzip, deflate"},
      {"sec-websocket-version", "13"}, {"origin", "http://<user>"},
      {"sec-websocket-extensions", "permessage-deflate"},
      {"sec-websocket-key", "S82iJ2lUI+jBYrl9P6icQQ=="},
      {"cookie", "_my_server_key=snip"},
      {"connection", "keep-alive, Upgrade"},
      {"pragma", "no-cache"},
      {"cache-control", "no-cache"},
      {"upgrade", "websocket"}
    ],
    p_headers: [
      {"sec-websocket-extensions", [{"permessage-deflate", []}]},
      {"upgrade", ["websocket"]},
      {"connection", ["keep-alive", "upgrade"]}
    ],
    cookies: :undefined,
    meta: [websocket_version: 13, websocket_compress: false],
    body_state: :waiting,
    buffer: "",
    multipart: :undefined,
    resp_compress: false,
    resp_state: :done,
    resp_headers: [],
    resp_body: "",
    onresponse: :already_called
  ],
  state: %{proxy: nil, state: %{}}
]
  (cowboy) c:/Users/<user>/Projects/my_server/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Specifically it is complaining about the division on line 37 in the lib/wobserver/system/scheduler.ex file, the whole function is:

  def utilization do
    ensure_started()

    case last_utilization() do
      false ->
        get_utilization()
        |> Enum.map(fn {_, u, t} ->u / t end)
      last ->
        get_utilization()
        |> Enum.zip(last)
        |> Enum.map(fn {{_, u0, t0}, {_, u1, t1}} -> (u1 - u0) / (t1 - t0) end)
    end
  end

I also see the error happen on the other Enum.map line as well, specifically it seems t can be 0 in the first Enum.map and t1-t0 can be 0 in the other Enum.map at times, checks probably need to be added to do an alternative action instead.

Doesn't work with distillery releases

I'm giving this a shot in one of our elixir applications, and it works great in standalone mode and plug mode when running locally. But when we deploy to stage and prod, we use distillery releases. I think the distillery release breaks the way wobserver serves the index.html, because the file is not included in the release. This is the error we get:

Elixir.File.Error: could not read file stats "deps/wobserver/assets/index.html": no such file or directory
  File "lib/file.ex", line 289, in File.stat!/2
  File "lib/plug/adapters/cowboy/conn.ex", line 38, in Plug.Adapters.Cowboy.Conn.send_file/6
  File "lib/plug/conn.ex", line 429, in Plug.Conn.send_file/5
  File "lib/wobserver/web/router/static.ex", line 1, in Wobserver.Web.Router.Static.plug_builder_call/2
  File "lib/plug/router/utils.ex", line 91, in Plug.Router.Utils.forward/4
  File "lib/wobserver/web/router.ex", line 1, in Wobserver.Web.Router.plug_builder_call/2
  File "lib/phoenix/router/route.ex", line 154, in Phoenix.Router.Route.forward/4
  File "lib/phoenix/router.ex", line 261, in API.Router.dispatch/2

Any chance you could give distillery a shot to see how we could get around this?

Use nodes in combination with load balancer

Was wondering how to configure the nodes section when you have like 3 nodes behind a load balancer which you can't directly access though they can connect with each other off-course. Was expecting that if you configure the nodes you can see the web interface through 1 node and that one connects to the other nodes with the switcher. Maybe my view of how the node version should work isn't correct.

Was trying to configure it in combination with the plug mode. But then when I switched I got a lot of /ws endpoint not found errors on the webserver side.

External Javascript

Manually built and packed assets and got other things ready and it 'mostly' works now it seems, however Load Charts is entirely blank, Memory Allocators are entirely blank, Application only shows the drop-down box and description, it looks like a javascript library is supposed to be making the tables sortable in various other pages, however the javascript (and apparently some fonts) are attempting to be called externally, which does not work on the internal network server I'm running. ^.^

The traditionally Phoenix way to do it is using its bundler (brunch by default, but can replace with whatever, or a manual one) to bake everything together for ease of use in such situations, though this is not Phoenix based it is still a useful style for such locations as here. :-)

How to start wobserver only in development.

I have wobserver working in my application, However I want it to only run when I need it.

My solution was to remove :wobserver from the applications list in mix.exs. When I need wobserver I start an iex session on the node and call Application.ensure_all_started(:wobserver). However doing this does and the visiting the wobserver port and I get the error that the site cannot be reached. Apart from removing wobserver from my applications list I have made no change to the setup that was working.

Not working and causing conflict with new lib

HTTPoison is old (1.3.1 is out)
Plug is old (1.7 is out)
Cowboy is old (2.5 is out)

makes it really hard to use it as it starts to create a lot of conflich with existing newer deps

Discovery of other nodes in a cluster?

In following instructions and looking through the source, it appears that the discovery of other nodes is limited to trying to connect to another Wobserver instance?

I was hoping it might be like Observer where I can have it display the information of other connected/clustered nodes. Is that a possibility? Or does Wobserver have to be installed on every node in the cluster that we might want to inspect?

Because of dependency issues, I created a small node with only Wobserver installed that could join an existing cluster and from there review/monitor all the nodes in the cluster.

Is this currently possible? If not, is it planned for the future?

Asset paths require trailing slash to be present in the URL

I have added wobserver 0.1.5 to my phoenix app.

My config is

config :wobserver,
  mode: :plug,
  remote_url_prefix: "/wobserver"

and I have added forward "/wobserver", Wobserver.Web.Router to the router.

It seems that if I open

http://localhost:4060/wobserver/ (notice slash at the end) it works fine but if I omit the last slash it tries to fetch assets from http://localhost:4060/app.js etc.

Disconnect does not notify or reconnect

A disconnect does not alert or reconnect socket.

Improve connection loss handling by:

  • Alert/notify user
  • Gracefully reconnect
  • Keep websocket, if able to connected to websocket before. (no-fallback)

Connection Lost on Heroku

image

Source code for the application is https://github.com/mgwidmann/slack_coder
I've been trying to figure this out with no luck on what is happening.

Heres the logs from Heroku, as you can see Heroku picks up on the GET /wobserver/ws call to make the socket connect but Phoenix never logs it out anywhere (typically it doesn't log anything until the join). The browser gets returned back a 204 No Content.

### First, heres me hitting the observer

2017-05-12T02:28:41.645622+00:00 heroku[router]: at=info method=GET path="/wobserver/" host=slack-coder.herokuapp.com request_id=34a800f3-ae02-4271-b348-8718b55387ac fwd="173.79.136.174" dyno=web.1 connect=1ms service=1ms status=200 bytes=1167 protocol=https
2017-05-12T02:28:41.706605+00:00 heroku[router]: at=info method=GET path="/wobserver/app.js" host=slack-coder.herokuapp.com request_id=7cb447de-dccd-4348-b385-b64ff3d6012b fwd="173.79.136.174" dyno=web.1 connect=0ms service=7ms status=200 bytes=295092 protocol=https
2017-05-12T02:28:41.675036+00:00 heroku[router]: at=info method=GET path="/wobserver/main.css" host=slack-coder.herokuapp.com request_id=c6968364-00ab-4236-af54-9395d1a1c00a fwd="173.79.136.174" dyno=web.1 connect=0ms service=2ms status=200 bytes=39690 protocol=https
2017-05-12T02:28:41.644613+00:00 app[web.1]: 02:28:41.644 request_id=34a800f3-ae02-4271-b348-8718b55387ac [info] GET /wobserver/
2017-05-12T02:28:41.644627+00:00 app[web.1]: 02:28:41.644 request_id=34a800f3-ae02-4271-b348-8718b55387ac [info] Sent 200 in 160µs
2017-05-12T02:28:41.673737+00:00 app[web.1]: 02:28:41.672 request_id=c6968364-00ab-4236-af54-9395d1a1c00a [info] GET /wobserver/main.css
2017-05-12T02:28:41.673780+00:00 app[web.1]: 02:28:41.672 request_id=c6968364-00ab-4236-af54-9395d1a1c00a [info] Sent 200 in 78µs
2017-05-12T02:28:41.698442+00:00 app[web.1]: 02:28:41.697 request_id=7cb447de-dccd-4348-b385-b64ff3d6012b [info] GET /wobserver/app.js
2017-05-12T02:28:41.698447+00:00 app[web.1]: 02:28:41.698 request_id=7cb447de-dccd-4348-b385-b64ff3d6012b [info] Sent 200 in 55µs
2017-05-12T02:28:41.861420+00:00 heroku[router]: at=info method=GET path="/wobserver/ws" host=slack-coder.herokuapp.com request_id=8d3b5cf2-8cd2-48b2-9731-d9c375f48ae4 fwd="173.79.136.174" dyno=web.1 connect=0ms service=1ms status=204 bytes=99 protocol=https

### Heres me hitting my index route path which has a successfully implemented phoenix socket on it... Odd it does not have a heroku router GET call...

2017-05-12T02:29:13.284211+00:00 heroku[router]: at=info method=GET path="/" host=slack-coder.herokuapp.com request_id=e338d64f-50e5-40a0-b37e-57681bef7fd3 fwd="173.79.136.174" dyno=web.1 connect=0ms service=6ms status=200 bytes=12101 protocol=https
2017-05-12T02:29:13.315163+00:00 heroku[router]: at=info method=GET path="/css/app-f60f3ae0a7de39b988f6262c7c365a52.css?vsn=d" host=slack-coder.herokuapp.com request_id=dc3dfdc4-d52c-4923-97b8-d5e3045f1509 fwd="173.79.136.174" dyno=web.1 connect=0ms service=1ms status=200 bytes=1568 protocol=https
2017-05-12T02:29:13.278441+00:00 app[web.1]: 02:29:13.277 request_id=e338d64f-50e5-40a0-b37e-57681bef7fd3 [info] GET /
2017-05-12T02:29:13.282977+00:00 app[web.1]: 02:29:13.282 request_id=e338d64f-50e5-40a0-b37e-57681bef7fd3 [info] Sent 200 in 4ms
2017-05-12T02:29:13.469617+00:00 heroku[router]: at=info method=GET path="/images/code-climate-grey-eb25b8d23eac8ebfba0bbdcfbc5f5cbb.svg?vsn=d" host=slack-coder.herokuapp.com request_id=55bad725-b6b5-4b52-bee6-482f81b6ae06 fwd="173.79.136.174" dyno=web.1 connect=0ms service=2ms status=200 bytes=870 protocol=https
2017-05-12T02:29:13.339947+00:00 heroku[router]: at=info method=GET path="/js/app-9c01173be1eb302965bc17dfc8c823cb.js?vsn=d" host=slack-coder.herokuapp.com request_id=a9c2a67a-b9bc-420f-9b35-7bc489a622ce fwd="173.79.136.174" dyno=web.1 connect=0ms service=5ms status=200 bytes=102833 protocol=https
2017-05-12T02:29:13.895743+00:00 app[web.1]: 02:29:13.895 [info] JOIN prs:all to SlackCoder.PRChannel
2017-05-12T02:29:13.895754+00:00 app[web.1]:   Transport:  Phoenix.Transports.WebSocket
2017-05-12T02:29:13.895755+00:00 app[web.1]:   Parameters: %{"github" => "mgwidmann"}
2017-05-12T02:29:13.896252+00:00 app[web.1]: 02:29:13.896 [info] Replied prs:all :ok

It seems the backup JSON API is also being blocked because its running on https and its using http only.

Security issue when displaying process state

Hi!

I was inspecting the state of my process that contained some content from a blog post, when suddenly I saw:

capture du 2017-07-05 19-40-41

The state of the process is interpreted directly in the html, which could lead to a possible code injection problem. If I had some script tags in it, it would have been evaluated, god knows what could have happened.

I'm using version 0.1.7 of wobserver.
I can't issue a PR right now with my sucky internet, but if you want to correct the misbehavior, the code involved is around here.

I might sound rude, but I like this software, I'm not blaming anyone 🤗

module Wobserver.Assets is not available

When freshly mix deps.compile wobserver this as a dependency, the compilation gives these warnings, and indeed I'm not seeing a Wobserver.Assets module in the project?

==> wobserver
Compiling 4 files (.ex)
warning: function Wobserver.Assets.html/0 is undefined (module Wobserver.Assets is not available)
  lib/wobserver/web/router/static.ex:21

warning: function Wobserver.Assets.css/0 is undefined (module Wobserver.Assets is not available)
  lib/wobserver/web/router/static.ex:32

warning: function Wobserver.Assets.js/0 is undefined (module Wobserver.Assets is not available)
  lib/wobserver/web/router/static.ex:38

warning: function Wobserver.Assets.license/0 is undefined (module Wobserver.Assets is not available)
  lib/wobserver/web/router/static.ex:43

Limit concurrency on `structure_pid` calls

We're on a fork of an earlier version of wobserver, but I thought I'd bring this issue up in the core repo as an issue.

We've been seeing consistent timeouts from this parallel_map call: https://github.com/shinyscorpion/wobserver/blob/master/lib/wobserver/util/application.ex#L97. We've also noticed that the BEAM load spikes pretty hard whenever we navigate to the wobserver page for one of our apps that has many processes (~100 Ranch acceptors under a single supervisor). I'm guessing this has to do with all of the core :erlang calls getting process info

Here's a stacktrace from our version:

14:40:38.116 [error] Task #PID<0.1046.0> started from #PID<0.1033.0> terminating
** (stop) exited in: Task.await(%Task{owner: #PID<0.1046.0>, pid: #PID<0.1056.0>, ref: #Reference<0.0.1.97714>}, 5000)
    ** (EXIT) time out
    (elixir) lib/task.ex:416: Task.await/2
    (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
    (wobserver) lib/wobserver/util/application.ex:101: Wobserver.Util.Application.structure_pid/1
    (elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
    (elixir) lib/task/supervised.ex:36: Task.Supervised.reply/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: #Function<1.105204156/0 in Wobserver.Util.Helper.parallel_map/2>
    Args: []
14:40:38.116 [error] Ranch protocol #PID<0.1024.0> (:cowboy_protocol) of listener TaskRunner.Endpoint.HTTP terminated
** (exit) exited in: Task.await(%Task{owner: #PID<0.1046.0>, pid: #PID<0.1056.0>, ref: #Reference<0.0.1.97714>}, 5000)
    ** (EXIT) time out

When this time out happens the entire wobserver page fails to load which has made it unusable in app without changes.

I'd suggest using a pool to limit concurrency or chunking the processes

Aggregate node information in single display

Make it possible for selected pages to show the information of all nodes in the network.

Involves the following steps:

  • Make the JSON api work with the special node name: all, which aggregates the API results for all nodes.
  • Make the websocket capable of aggregating
  • Upgrade interface for aggregated data.

The configurable security setup doesn't actually work

This is in my config.exs file:

config :wobserver,
  mode: :plug,
  remote_url_prefix: "/wobserver",
  security: BorsNG.WobserverSecurity

This is my authenticate function:

defmodule BorsNG.WobserverSecurity do
  @spec authenticate(Conn.t) :: Conn.t
  def authenticate(conn) do
    raise "HERE"
  end
end

It doesn't reach the raise "HERE" line when I open the wobserver page.

This is because the module attributes are evaluated at build-time, which uses the files in "wobserver"'s configuration, not in the dependent modules.

Wobserver as a Plug behind proxy

I'm using wobserver in a Phoenix-Application (configured as plug).
Locally everything works just fine but when I run the application on the production server, I have the following problem:

  • App is running on port 4000
  • NGINX-Proxy listens on https://...., signs the communication and uses proxy_pass localhost:4000 to hit the application.
  • Wobserver starts fine at https://..../metrics but then can't connect the websocket because it is still a ws instead of wss transport.

It seems there is no way yet to configure wss, right?

Manual Instructions incorrect

When running through the manual instructions step on the readme the gulp build step gives the gulp: command not found because the step as typed in is trying to call a global gulp command although the project installs a local gulp that has to be called via ./node_modules/.bin/gulp, thus the command should probably be ./node_modules/.bin/gulp build.

Scheduler usage when using websocket

There is a higher than expected scheduler usage, when using websockets.

Look into whether this is just websocket related or something in the communication between wobserver and javascript interface.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.