Code Monkey home page Code Monkey logo

proxy-wasm-go-sdk's Introduction

WebAssembly for Proxies (Go SDK) Build License

The Go SDK for Proxy-Wasm, enabling developers to write Proxy-Wasm plugins in Go. This SDK is powered by TinyGo and does not support the official Go compiler.

Getting Started

  • examples directory contains the example codes on top of this SDK.
  • OVERVIEW.md the overview of Proxy-Wasm, the API of this SDK, and the things you should know when writing plugins.

Requirements

  • TinyGo - This SDK depends on TinyGo and leverages its WASI (WebAssembly System Interface) target. Please follow the official instruction here for installing TinyGo.
  • Envoy - To run compiled examples, you need to have Envoy binary. We recommend using func-e as the easiest way to get started with Envoy. Alternatively, you can follow the official instruction.

Dealing with memory issues

TinyGo's default memory allocator (Garbage Collector) is known to have some issues when it's used in the high workload environment (e.g. 1,2). There's an alternative GC called nottinygc which not only resolves the memory related issues, but also improves the performance on production usage.

The following images are an end user's observation on the perf of their Go SDK-compiled plugin on a high-workload environment. This clearly indicates that nottinygc performs pretty well compared to the default setting of TinyGo.

img img

It can be enabled by adding a single line in your source code. Please refer to https://github.com/wasilibs/nottinygc for detail.

Installation

go get github.com/tetratelabs/proxy-wasm-go-sdk

Build and run Examples

# Build all examples.
make build.examples

# Build a specific example.
make build.example name=helloworld

# Run a specific example.
make run name=helloworld

Compatible Envoy builds

Envoy is the first host side implementation of Proxy-Wasm ABI, and we run end-to-end tests with multiple versions of Envoy and Envoy-based istio/proxy in order to verify Proxy-Wasm Go SDK works as expected.

Please refer to workflow.yaml for which version is used for End-to-End tests.

Build tags

The following build tags can be used to customize the behavior of the built plugin:

  • proxywasm_timing: Enables logging of time spent in invocation of the plugin's exported functions. This can be useful for debugging performance issues.

Contributing

We welcome contributions from the community! See CONTRIBUTING.md for how to contribute to this repository.

External links

proxy-wasm-go-sdk's People

Contributors

anuraaga avatar boeboe avatar bogdanciuca avatar bzp2010 avatar codefromthecrypt avatar dgryski avatar doriandekoning avatar evacchi avatar freegroup avatar glenn-m avatar ikeeip avatar jamesmulcahy avatar jcchavezs avatar jlourenc avatar jmjolysc avatar jonathanvila avatar m4tteop avatar mathetake avatar morepork avatar nacx avatar omidtavakoli avatar peterj avatar rchernobelskiy avatar spacewander avatar tkrns avatar while1malloc0 avatar wimspaargaren avatar wouterposdijk avatar yike21 avatar zagidziran 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

proxy-wasm-go-sdk's Issues

SetHttpRequestBody Is bad argument

ENV:

envoy version: 5c801b25cae04f06bf48248c90e87d623d7a6283/1.17.0/Modified/DEBUG/BoringSSL
tinygo v0.16.0.

Hi

I refer to examples/http_body

func (ctx *context) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
	proxywasm.LogInfof("body size: %d", bodySize)
	if bodySize != 0 {
		initialBody, err := proxywasm.GetHttpRequestBody(0, bodySize)
		if err != nil {
			proxywasm.LogErrorf("failed to get request body: %v", err)
			return types.ActionContinue
		}
		proxywasm.LogInfof("initial request body: %s", string(initialBody))
		err = proxywasm.SetHttpRequestBody([]byte(`{"hello":"henry"}`))
		if err != nil {
			proxywasm.LogErrorf("failed to set request body: %v", err)
			return types.ActionContinue
		}
		proxywasm.LogInfof("on http request body finished")
	}

	return types.ActionContinue
}

envoy debug

[2021-02-03 16:20:45.965][1141921][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: body size: 5
[2021-02-03 16:20:45.965][1141921][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: initial request body: {"hello":"Alice"}
[2021-02-03 16:20:45.965][1141921][error][wasm] [source/extensions/common/wasm/context.cc:1177] wasm log: failed to set request body: error status returned by host: bad argument

but in the Test case everything is ok, So why failed to set request body: error status returned by host: bad argument

thanks

Segfault with SetHttpResponseHeaders

std::terminate called! (possible uncaught exception, see trace)
Backtrace (use tools/stack_decode.py to get line numbers):
Envoy version: 7a49df81214b249c9928a2e77e3bd4e540fa6ab1/1.18.0-dev/Clean/DEBUG/BoringSSL
#0: Envoy::TerminateHandler::logOnTerminate()::$_0::operator()() [0x56202304c184]
#1: Envoy::TerminateHandler::logOnTerminate()::$_0::__invoke() [0x56202304bf69]
#2: __cxxabiv1::__terminate() [0x562023e98b06]
Caught Aborted, suspect faulting address 0x3e800007f10
Backtrace (use tools/stack_decode.py to get line numbers):
Envoy version: 7a49df81214b249c9928a2e77e3bd4e540fa6ab1/1.18.0-dev/Clean/DEBUG/BoringSSL
#0: Envoy::SignalAction::sigHandler() [0x562022dde169]
#1: __restore_rt [0x7fa3905363c0]
#2: Envoy::TerminateHandler::logOnTerminate()::$_0::__invoke() [0x56202304bf69]
#3: __cxxabiv1::__terminate() [0x562023e98b06]
ActiveStream 0x57d33dc67600, stream_id_: 4762672115198020022&filter_manager_:

Call for examples addition

It would be great to have examples in this SDK as many as possible under examples/ directory. Please feel free to add and send PRs 😉

Is there an update?

This is a great project and I have been studying wasm-go recently. Is there any update? thanks

time.Now does not work

almost similar to #67 and clock_time_get is not implemented in host yet

Failed to load Wasm module due to a missing import: wasi_unstable.clock_time_get

Unable to create Wasm HTTP filter my_plugin

I cannot run the demo program,I use the latest version of Envoy。

FROM envoyproxy/envoy-dev:latest
COPY ./envoy.yaml /etc/envoy.yaml
COPY ./lib/envoy_filter_http_wasm_example.wasm /lib/envoy_filter_http_wasm_example.wasm
RUN chmod go+r /etc/envoy.yaml /lib/envoy_filter_http_wasm_example.wasm
CMD ["/usr/local/bin/envoy", "-c", "/etc/envoy.yaml", "--service-cluster", "proxy", "-l", "debug"]

image

Envoy Config:

static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 8000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: web_service
          http_filters:
          - name: envoy.filters.http.wasm
            typed_config:
              "@type": type.googleapis.com/udpa.type.v1.TypedStruct
              type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
              value:
                config:
                  name: "my_plugin"
                  root_id: "my_root_id"
                  configuration:
                    "@type": "type.googleapis.com/google.protobuf.StringValue"
                    value: |
                      {}
                  vm_config:
                    runtime: "envoy.wasm.runtime.v8"
                    vm_id: "my_vm_id"
                    code:
                      local:
                        filename: "lib/envoy_filter_http_wasm_example.wasm"
                    configuration: {}
          - name: envoy.filters.http.router
            typed_config: {}
  clusters:
  - name: web_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    load_assignment:
      cluster_name: service1
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: web_service
                port_value: 9000
admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

Golang Code:

package main

import (
	"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
	"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {
	proxywasm.SetNewHttpContext(newContext)
}

type httpHeaders struct {
	// you must embed the default context so that you need not to reimplement all the methods by yourself
	proxywasm.DefaultHttpContext
	contextID uint32
}

func newContext(rootContextID, contextID uint32) proxywasm.HttpContext {
	return &httpHeaders{contextID: contextID}
}

// override
func (ctx *httpHeaders) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
	hs, err := proxywasm.GetHttpRequestHeaders()
	if err != nil {
		proxywasm.LogCriticalf("failed to get request headers: %v", err)
	}

	for _, h := range hs {
		proxywasm.LogInfof("request header --> %s: %s", h[0], h[1])
	}
	return types.ActionContinue
}

// override
func (ctx *httpHeaders) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
	hs, err := proxywasm.GetHttpResponseHeaders()
	if err != nil {
		proxywasm.LogCriticalf("failed to get request headers: %v", err)
	}

	for _, h := range hs {
		proxywasm.LogInfof("response header <-- %s: %s", h[0], h[1])
	}
	return types.ActionContinue
}

// override
func (ctx *httpHeaders) OnHttpStreamDone() {
	proxywasm.LogInfof("%d finished", ctx.contextID)
}

The version information of tinygo is as follows

tinygo version                                       
tinygo version 0.15.0 darwin/amd64 (using go version go1.15.3 and LLVM version 10.0.1)

The build method is as follows

tinygo build -o ./main.go.wasm -target=wasm -wasm-abi=generic ./main.go

DispatchHttpCall from OnTick causes 'runtime error: nil pointer dereference'

I want to load and cache some data into the filter.

func (ctx *rootContext) OnTick() {
	updateRules()
}

func updateRules() {
	hs := [][2]string{{":method", "GET"}, {":authority", "some_authority"}, {":path", "/path/to/service"}, {"accept", "*/*"}}

	if _, err := proxywasm.DispatchHttpCall(clusterName, hs, "", [][2]string{},
		5000, httpCallResponseCallback1); err != nil {
		proxywasm.LogCriticalf("dipatch1 httpcall failed: %v", err)
	}
}

And it causes 'runtime error: nil pointer dereference'. Is it a bug ? Is it possible somehow to solve this ?

Proxy-Wasm extension out of memory

I found out of memory problems with wasm extension in the following two situations:

  1. Define a large structure and instantiate it in the wasm extension, and then run the wasm extension.
  2. Use Apache Jmeter(5 Threads) to evaluate the performance of wasm extension.

So, I want to know how to set the maximum memory each VM can allocate? Can someone help me?

time.Sleep does not work

With the following code:

// override
func (ctx faultInjection) OnHttpRequestHeaders(int, bool) types.Action {
	if time.Now().UnixNano()%100 < 5 {
		time.Sleep(time.Millisecond * 500)
		proxywasm.LogInfo("sleep for 500 milliseconds...")
	}
	return types.ActionContinue
}

we have the following compile error

tinygo build -o ./examples/fault_injection/main.go.wasm -target=wasi -wasm-abi=generic ./examples/fault_injection/main.go
panic: trying to make exported function async: proxy_on_request_headers

goroutine 1 [running]:
github.com/tinygo-org/tinygo/transform.(*coroutineLoweringPass).findAsyncFuncs(0xc003e7b040)
        /home/circleci/project/transform/coroutines.go:186 +0xa64
github.com/tinygo-org/tinygo/transform.(*coroutineLoweringPass).load(0xc003e7b040, 0x20, 0x7f79bca925f0)
        /home/circleci/project/transform/coroutines.go:305 +0x36c
github.com/tinygo-org/tinygo/transform.LowerCoroutines(0x838d8b0, 0x838d801, 0x0, 0x0)
        /home/circleci/project/transform/coroutines.go:70 +0x15f
github.com/tinygo-org/tinygo/transform.Optimize(0x838d8b0, 0xc000267dd0, 0x2, 0x2, 0x5, 0x0, 0x0, 0x0)
        /home/circleci/project/transform/optimizer.go:120 +0xef0
github.com/tinygo-org/tinygo/builder.Build(0x7ffe3611dae1, 0x22, 0x7ffe3611da9a, 0x27, 0xc000267dd0, 0xc003e7bba8, 0x0, 0x0)
        /home/circleci/project/builder/build.go:109 +0x337a
main.Build(0x7ffe3611dae1, 0x22, 0x7ffe3611da9a, 0x27, 0xc0002a8000, 0x0, 0x12)
        /home/circleci/project/main.go:99 +0xc5
main.main()
        /home/circleci/project/main.go:909 +0x2071
make: *** [Makefile:10: build.example] Error 2

This seems something to do with scheduler

modify l4 buffer

I'm trying to modify the upstream data being sent back from a network filter and encountering a bad argument status from

func ProxySetBufferBytes(bt types.BufferType, start int, maxSize int, bufferData *byte, bufferSize int) types.Status

I thought perhaps #45 may have covered this case, but the implementations seem to be specific to l7 filters.

Is this currently supported and I'm just setting the wrong buffer type or is it currently not supported by the sdk, cpp-host, or wasi?

Cannot build the examples due to package not found error.

Hi,

I am trying to experiment with the go SDK and encountered following issue when building the examples.

$tinygo build -o wasm.wasm -wasm-abi=generic -target wasm ./main.go && docker-compose up | grep OnTick

/main.go: cannot find package "github.com/mathetake/proxy-wasm-go/runtime" in any of:
	/usr/local/go/src/github.com/mathetake/proxy-wasm-go/runtime (from $GOROOT)
	/Users/menakajayawardena/go/src/github.com/mathetake/proxy-wasm-go/runtime (from $GOPATH)

Could you please let me know if anything to be done to overcome this?
Thanks

Does it support running in debug mode in IDE?

How to support developers to run programs in debugging mode?
I tried to get a compilation error:

# github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/rawhostcall
../../proxywasm/rawhostcall/rawhostcall.go:24:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:27:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:31:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:34:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:37:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:40:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:43:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:46:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:49:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:52:6: missing function body
../../proxywasm/rawhostcall/rawhostcall.go:52:6: too many errors

image

add unit test framework

  • network filter
  • root contex
  • http context + http related host calls
  • http callouts
  • shared queue
  • shared data
  • metrics

Is it possible reject http request `OnHttpRequestHeaders`

I've tried this demo code

func (ctx *httpHeaders) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
	hs, err := proxywasm.GetHttpRequestHeaders()
	token := // get token from header
	if len(token) == 0 {  // token does not exist
		proxywasm.SendHttpResponse(401, [][2]string{
			{"powered-by", "proxy-wasm-go-sdk!!"},
		}, "reject")
	}
	return types.ActionContinue
}

and got error

envoy bug failure: !state_.local_complete_ || status == FilterHeadersStatus::StopIteration. Details: Filters should return FilterHeadersStatus::StopIteration after sending a local reply.
Caught Abort trap: 6, suspect faulting address 0x7fff20307462
Backtrace (use tools/stack_decode.py to get line numbers):
Envoy version: d6a4496e712d7a2335b26e2f76210d5904002c26/1.17.1/Modified/DEBUG/BoringSSL
#0: Envoy::SignalAction::sigHandler() [0x118340bdc]
#1: _sigtramp [0x7fff20379d7d]
#2: [0x0]
#3: abort [0x7fff20288720]
#4: Envoy::Http::FilterManager::decodeHeaders() [0x116a99b0f]
#5: Envoy::Http::FilterManager::decodeHeaders() [0x1169d4dd6]
#6: Envoy::Http::ConnectionManagerImpl::ActiveStream::decodeHeaders() [0x1169d3976]
#7: Envoy::Http::Http1::ServerConnectionImpl::onMessageComplete() [0x116b9d34a]
#8: Envoy::Http::Http1::ConnectionImpl::onMessageCompleteBase() [0x116b9932d]
#9: Envoy::Http::Http1::ConnectionImpl::$_8::operator()() [0x116ba9440]
#10: Envoy::Http::Http1::ConnectionImpl::$_8::__invoke() [0x116b8e2e5]
#11: http_parser_execute [0x1181a2d30]
#12: Envoy::Http::Http1::ConnectionImpl::dispatchSlice() [0x116b93b44]
#13: Envoy::Http::Http1::ConnectionImpl::innerDispatch() [0x116b92306]
#14: Envoy::Http::Http1::ConnectionImpl::dispatch()::$_14::operator()() [0x116be190e]
#15: std::__1::__invoke<>() [0x116be18bb]
#16: std::__1::__invoke_void_return_wrapper<>::__call<>() [0x116be183b]
#17: std::__1::__function::__alloc_func<>::operator()() [0x116be17cb]
#18: std::__1::__function::__func<>::operator()() [0x116be04cd]
#19: std::__1::__function::__value_func<>::operator()() [0x117f3f18b]
#20: std::__1::function<>::operator()() [0x117f02946]
#21: Envoy::Http::Utility::exceptionToStatus() [0x117f02618]
#22: Envoy::Http::Http1::ConnectionImpl::dispatch() [0x116b90e53]
#23: Envoy::Http::Http1::ConnectionImpl::dispatch() [0x116b90f3b]
#24: Envoy::Http::ConnectionManagerImpl::onData() [0x1169c8bf4]
#25: Envoy::Network::FilterManagerImpl::onContinueReading() [0x114e79791]
#26: Envoy::Network::FilterManagerImpl::onRead() [0x114e7a012]
#27: Envoy::Network::ConnectionImpl::onRead() [0x114df6454]
#28: Envoy::Network::ConnectionImpl::onReadReady() [0x114dffdb1]
#29: Envoy::Network::ConnectionImpl::onFileEvent() [0x114dfc3e0]
#30: Envoy::Network::ConnectionImpl::ConnectionImpl()::$_6::operator()() [0x114e409be]
#31: std::__1::__invoke<>() [0x114e40981]
#32: std::__1::__invoke_void_return_wrapper<>::__call<>() [0x114e40922]
#33: std::__1::__function::__alloc_func<>::operator()() [0x114e408d2]
#34: std::__1::__function::__func<>::operator()() [0x114e3f5f3]
#35: std::__1::__function::__value_func<>::operator()() [0x114d0144d]
#36: std::__1::function<>::operator()() [0x114d013ef]
#37: Envoy::Event::DispatcherImpl::createFileEvent()::$_3::operator()() [0x114d013b4]
#38: std::__1::__invoke<>() [0x114d01361]
#39: std::__1::__invoke_void_return_wrapper<>::__call<>() [0x114d01302]
#40: std::__1::__function::__alloc_func<>::operator()() [0x114d012b2]
#41: std::__1::__function::__func<>::operator()() [0x114cffe83]
#42: std::__1::__function::__value_func<>::operator()() [0x114d0144d]
#43: std::__1::function<>::operator()() [0x114d013ef]
#44: Envoy::Event::FileEventImpl::mergeInjectedEventsAndRunCb() [0x114d0b5d5]
#45: Envoy::Event::FileEventImpl::assignEvents()::$_1::operator()() [0x114d0bb83]
#46: Envoy::Event::FileEventImpl::assignEvents()::$_1::__invoke() [0x114d0b786]
#47: event_persist_closure [0x11818f0e5]
#48: event_process_active_single_queue [0x11818e718]
#49: event_process_active [0x118188ee4]
#50: event_base_loop [0x118187dab]
#51: Envoy::Event::LibeventScheduler::run() [0x118077af4]
#52: Envoy::Event::DispatcherImpl::run() [0x114cc820a]
#53: Envoy::Server::WorkerImpl::threadRoutine() [0x114bd43e5]
#54: Envoy::Server::WorkerImpl::start()::$_5::operator()() [0x114c0804c]
#55: std::__1::__invoke<>() [0x114c0800d]
#56: std::__1::__invoke_void_return_wrapper<>::__call<>() [0x114c07fbd]
#57: std::__1::__function::__alloc_func<>::operator()() [0x114c07f8d]
#58: std::__1::__function::__func<>::operator()() [0x114c06cbe]
#59: std::__1::__function::__value_func<>::operator()() [0x10ab1fb55]
#60: std::__1::function<>::operator()() [0x10ab1f925]

Perform common actions with proxy wasm go

Hi,

While experimenting with the SDK, I was trying to do actions like JSON decoding the headers etc.

The code is compiling but throws an error when starting up envoy.

proxy_1 | Failed to load WASM module due to a missing import: env.syscall/js.valueGet
I was also trying to do io stuff like reading a file etc. But got the same error.

What could be the reason for this? Would there be any workaround which can be done in the sdk level to fix this?

Rebuild latest docker container

I was wondering if it would be possible to rebuild the latest docker container for every master pipeline. Currently the newest image is 12 days old

I also noticed a rather annoying typo in the readme:
getenvoy/extention-tingyo-builder:wasi-dev
Should be:
getenvoy/extension-tingyo-builder:wasi-dev

Might be nice to add an example of building filters using the docker file in the REAMDE as well!

about lifecycle

I find some funcs such as proxy_get_header_map_value that don't include contextid,how can host impl to return the corrent request header ?

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.