Giter Club home page Giter Club logo

dynatrace-configuration-as-code's Introduction

Dynatrace Configuration as Code

Dynatrace Configuration as Code, an evolution from our Monitoring as Code CLI, provides Observability as Code and Security as Code to fully automate configuration of the Dynatrace platform at any scale, from automating the standard configuration of all your Dynatrace environments to meeting specific demands for individual environments.

The documentation for the Dynatrace Configuration as Code tool Monaco is available here.

Documentation for the previous 1.x versions is still available here

You can download the CLI as well as a copy of the Software Bill of Materials (SBOM) from the release page.
If you're new to Monaco and want to learn more, check out the Observability Clinic on Monaco 2.0.

Support for Monaco

The Dynatrace Configuration as Code tool Monaco is provided by Dynatrace Incorporated.
Support for Monaco 2.0+ is provided by the Dynatrace Support team, as described on the support page.

โš ๏ธ Older Monaco 1.x versions are not officially supported - we strongly suggest you upgrade to 2.x as soon as possible.

Feature ideas for Monaco

Please use the Dynatrace Community to let us know any Product Ideas for Monaco, as well as for general discussion and questions.

Contributing to Monaco

While Monaco development is driven by Dynatrace, we are still committed to keeping the tool open source to allow you to use it as a library or directly contribute improvements and features.
If you wish to use Monaco as a library, please make sure you have read and are complying with the terms of the Apache License v2.0.
If you wish to contribute to Monaco directly, please refer to the contributing guide to learn more about compiling Monaco and contributing changes.

License

Apache License v2.0.

Other forms of Dynatrace Configuration as Code

In addition to our official Configuration as Code tool Monaco, there is also the Dynatrace Terraform provider. Our documentation contains further information on when to use what tool.

dynatrace-configuration-as-code's People

Contributors

163a avatar agardnerit avatar arthurpitman avatar baumgarb avatar cdsre avatar centic9 avatar chanusch avatar churro avatar cruzancaramele avatar davidphirsch avatar dcryans avatar dependabot[bot] avatar didiladi avatar drqc avatar jskelin avatar kristofdynatrace avatar krzema12 avatar laubi avatar miigwi avatar neonalps avatar patrickb-dt avatar rszulgo avatar shschulze avatar tatomir146 avatar tobigremmer-dt avatar unseenwizzard avatar vincejayet avatar warber avatar ytsibizov-dt avatar zachbridges 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

dynatrace-configuration-as-code's Issues

Case Sensitive Configuration Type Folders

Describe the bug
Configuration type folder names are case sensitive. No configuration will be applied if folders are not lowercase.

But, no error is shown if folders are not lowercase.

How to reproduce
Steps to reproduce the behavior:
Create a folder called auto-tag with valid config. This will work:

monaco.exe --environments=environments.yaml
2020-11-20 08:57:32 INFO  Dynatrace Monitoring as Code v1.0.0
2020-11-20 08:57:32 INFO  Executing projects in this order:
2020-11-20 08:57:32 INFO        1: projects\baseconfig\auto-tag (1 configs)
2020-11-20 08:57:32 INFO  Processing environment my_environment...
2020-11-20 08:57:32 INFO        Processing project projects\baseconfig\auto-tag...
2020-11-20 08:57:32 INFO  Deployment finished without errors

Delete the tag rule and rename auto-tag to Auto-Tag. Re-run monaco:

monaco.exe --environments=environments.yaml
2020-11-20 08:57:32 INFO  Dynatrace Monitoring as Code v1.0.0
2020-11-20 08:57:32 INFO  Executing projects in this order:
2020-11-20 08:57:32 INFO        1: projects\baseconfig\Auto-Tag (0 configs)
2020-11-20 08:57:32 INFO  Processing environment my_environment...
2020-11-20 08:57:32 INFO        Processing project projects\baseconfig\Auto-Tag...
2020-11-20 08:57:32 INFO  Deployment finished without errors

This time, no tag rule is created.

Expected behavior
Either accept case insensitive foldernames or print a warning for invalid folder names.

Synthetic locations - unknown location

I found already a couple of times that when executing the synthetic set-up the Synthetic locations just stop to work with the unknown location error- Find below the error I've got:

020-11-20T15:28:28.9848035Z 2020-11-20 15:28:28 INFO Dynatrace Monitoring as Code v1.0.0
2020-11-20T15:28:29.0296087Z 2020-11-20 15:28:29 INFO Executing projects in this order:
2020-11-20T15:28:29.0296961Z 2020-11-20 15:28:29 INFO 1: projects\Application (9 configs)
2020-11-20T15:28:29.0297515Z 2020-11-20 15:28:29 INFO Processing environment Germany...
2020-11-20T15:28:29.0297841Z 2020-11-20 15:28:29 INFO Processing project projects\Application...
2020-11-20T15:28:29.8322009Z 2020-11-20 15:28:29 ERROR Deployment to Germany failed with error Failed to upsert DT object GE_APP-123_XXXXX (HTTP 400)!
2020-11-20T15:28:29.8323595Z Response was: {"error":{"code":400,"message":"Unknown location(s): [GEOLOCATION-5760CF2C54BBEE66]"}}, responsible config: projects\Application\synthetic-monitor\synthetic-monitor.json

Allow referencing configuration variables

Currently the Dynatrace Monitoring as Code tool only allows to reference the id/name of created Dynatrace objects

Referencing variables from another yaml configuration is not possible, but would be helpful.
E.g. to reference the names of metrics from a plugin configuration in a dashboard configuration

Problem with using flags

If using flags without equals "=" there is a bug if specifying project parameter as latest:
command... -p my_project

2020/04/16 11:07:02 Project my_project does not exist!: stat my_project/my_project: no such file or directory

Following works:
command... -p=my_project

The last parameter gets interpreted as configuration root parameter

Goland complains about missing references in integration tests

Describe the bug
When opening the project with Goland, you see the following error:
image

Unresolved references due to the variables folder and environmentsFile being defined in file multi_tenant_integration_test.go. Seems like this also inicates another bug, where we create configs for different environments, as we try to clean after running the integration tests. This also leads to the fact, that we try to clean up different configs, as we apply in the integration test run.

How to reproduce
Just start Goland

Expected behavior

  • The error is gone
  • Cleanup is run for the same environments as the configs are applied to
  • We clean up the same configs, as the tests creates

Log output
If applicable, add log snippet to help explain your problem.

Environment (please complete the following information):
All

Improve help text

When help text output is not really helpful, as it doesn't show all possibilities how
to execute monaco.

For example there is no info at all, how to specify which folder the project configurations
are in.

Add application detection rules ordering

For application detection rules, there is an API which enables a user to define the ordering of application detection rules.

Add support for this API in Dynatrace Monitoring as Code.

Config loads properties from other configs

There is currently an issue when defining multiple configs in a single yaml with similar names.

E.g. have two configs with one called dashboard and the other dashboard-availability.

dashboard will now add all properties from dashboard-avilability to its properties map.

This causes checks such as HasDependencyOn(Config) to als search for dependencies
on this wrong properties.

The cause for this is in conifg.go line 84:

func filterProperties(id string, properties map[string]map[string]string) map[string]map[string]string {

	result := make(map[string]map[string]string)
	for key, value := range properties {
		if strings.HasPrefix(key, id) {
			result[key] = value
		}
	}
	return result
}

Simply testing for HasPrefix isn't enough here.

INFO verbose logging

We would like to see a more verbose logging than currently is the case.

It would be interesting to see:

  • API call made
  • Response code

Support specifying "real" booleans (not strings) in yaml files

If a property defined in the yaml is a boolean, it feels counter-intuitive to write it as a string like: "true". This already caused confusion for a customer.

The following should be possible:

config:
  - email: "email.json"
email:
  ...
  - enabled: true

Instead of:

config:
  - email: "email.json"
email:
  ...
  - enabled: "true"

Validate that all config properties are used in template

It is quite easy by copy and paste pull create configs which have properties that are not used.

E.g.

dashboard:
  - name: "test"
  - testTicket: "hello"
{
  "name": {{ .name }}
}

When running dry run, it would be nice to point out that testTicket is unused.

Run parallel builds and integration tests on all supported platforms

Currently we build and test on Linux only, but release Linux, Windows and Mac versions of the tool.

To be more confident in those, we should run at least the integration tests on all platforms to continuously ensure that these releases function.

In trial we've already seen that there are the following issues with this:

  • linting check is using a shell script that is not compatible with windows (it does this to get around gofmt not returning error codes on unformated files)
  • integration tests running in parallel influence each other, resulting in some tests timing out or failing

Add support for API availability in the target environment

Dynatrace provides different APIs for configuration. Those APIs can change or there can be new APIs added. In order to be fault-tolerant and proactive, we should only perform the actions, the target environment supports.

Implement a preflight check for each target environment using the OpenSpecAPI
In case an API doesn't exist yet on this environment, log an information about the unavailable API
If the "not executed API call" is a prerequisite for another configuration, we have to cancel this one as well

Missing id error if using extension.name as dependency for extensions

If linking extension.name from one to another project, we get following error:

Id 'general/extension/some-id' was not available. Please make sure the reference exists.

There is a problem with checking Ids on extensions. id was not populated with .id (using old plugins API), but after switching to extensions API, now there is a id parameter on successful upload returned from api:

{
    "id": "custom.jmx.my-extension"
}

Improve cli design

As of right now, the cli is not really obvious to use. There are multiple
flags which are required, some which trigger different actions also we
have positional elements.

This ticket was created to kick off a discussion and collect proposals
on how to improve the cli design.

Change handling of backslash/slash in configs

As of right now, all / in the config will be replaced with \ on windows. This
could lead to some strange behaviors, such as replacing / in names.

This is done solely to handle paths on windows, but if you think about it, there
is no need to replace every / in the config, as we read files from disk only once
at the beginning. Afterwards / could always be used to reference other configs.

Support for Dynatrace Managed configuration as code

Apart from using monaco to configure a Dynatrace Environment configuration, it would also be great to have the ability to configure a Dynatrace Managed cluster.

Describe the solution you'd like
The ability to specify a managed cluster and token, and then configure the cluster using monaco

Endpoints that come to mind:

  • CRUD of a tenant
  • CRUD of Users/Groups
  • Cluster configuration itself

Support new SLO API

Is your feature request related to a problem? Please describe.
The new Dynatrace SLO API started as a preview feature in version 204. Customers, who are part of the preview should be able to leverage the SLO API using Monitoring as Code. Furthermore, when the feature goes GA, we already support it in monaco.

Describe the solution you'd like
Add support for the new SLO API and add a configuration used for the all-configs integration test.

Dry run to give diff overview

When doing a dry run, it would be great to see the actual diff between the current config in Dynatrace and what we are going to apply.

Reasoning is that in large environments you might loose oversight about what is being changed.

Potentially this output is stored in a file that can be added as an artefact to the build pipeline.

Deploy to different environments in paralel

Currently the MaC tool deploys to each environment in sequence.

For a large amount of environments that is an issue, as deployment takes a long time.
From a environment standpoint, there's no reason to not deploy to each in parallel.

In theory it should easily be possible to start a Goroutine for each execute and have them run in parallel rather than in sequence.

Main effort here is taking a close look at any issues we might run into from multithreading:

anything shared between those deployment calls?
any memory issues? e.g. creating a lot of new http client at the same time

Don't delete configs if apply didn't work

Currently, the tool deletes the configs in the delete.yaml file independently of the success of the prior applying of configs.

If the apply fails (for any config) we should skip the delete.

Unable to use variables within curly brackets

Defining a variable in JSON template within {} (without whitespaces) throws an error. At the moment only possibility it to define whole string in yaml which could be a bit cumbersome. This is e.g. needed if defining http monitor with credentials:

Api-Token {{{ .apiTokenId }}|token}

ERROR Could not execute template

I'm trying to execute a project called "General" that doesn't have any link with any other projects (./monaco -d -v -p="General" -e="./projects/tenants.yaml" projects) and when doing the "dry run", it seems to be doing a validation of another project called "Application", getting the following error message:

_```
" 2020-11-19 18:03:10 INFO Dynatrace Monitoring as Code v1.0.0
2020-11-19 18:03:10 ERROR Could not execute template: template: projects\Application\app-detection-rule\rules.yaml:8:21: executing "projects\Application\app-detection-rule\rules.yaml" at <.Env.URLPATTERN>: map has no entry for key "URLPATTERN"
2020-11-19 18:03:10 ERROR Error while converting file projects\Application\app-detection-rule\rules.yaml: template: projects\Application\app-detection-rule\rules.yaml:8:21: executing "projects\Application\app-detection-rule\rules.yaml" at <.Env.URLPATTERN>:
map has no entry for key "URLPATTERN"

Not doing basic validation on unrelated projects?

Related to #55
As @UnseenWizzard wrote
"In general the tool reads in all configs - at which point the env var resolution currently happens - in order to be able to handle dependencies between projects.
That is admittedly an imperfect solution for having several projects, or in-progress projects, because as you noticed they all are validated..."
I think this creates additional complexity because all project is validated and if any environment variable is not applicable for an specific project, you need to create all variables in all projects, otherwise it fails.

Allow configuration loops

It should be possible to define one configuration and apply it n times:

autotagging:
  - name: "Cluster[1..45]"
  - tag: "ClusterTag[1..45]"
  - process-group-name:
    - 1: "cluster_prod1-john"
    - 2: "cluster_prod2-mike"
    - 3: "cluster_prod3-tom"
    - 4: "cluster_prod4-michael"
      ...
    - 45: "cluster_prod45-whatsoever"

Counters [1..45] and lists should be supported:

- process-group-name:
    - 1: "cluster_prod1-john"
    - 2: "cluster_prod2-mike"
    - 3: "cluster_prod3-tom"
    - 4: "cluster_prod4-michael"

Allowed are only positive numbers. Counter doesn't have to start with 1.
Counters can be also defined as:

- name: "[1..45] Test"

or

- name: "Configuration [1..45] something"

Only one counter per parameter is allowed

name parameter defines how often is configuration applied and takes on every iteration it's incremental value:
name: "Test [1..45]" has following values: Test 1, Test 2 ... Test 45
If configuration parameters contains counter or list, and name doesn't, then config validation should fail:

autotagging:
  - name: "Cl-Prod"
  - tag: "Cluster[1..45]"
  - process-group-name:
    - 1: "cluster_prod1-john"
    - 2: "cluster_prod2-mike"
    - 3: "cluster_prod3-tom"
    - 4: "cluster_prod4-michael"

The counter size or number of elements in the list must match for all parameters. Something like this is not allowed:

  - name: "Cl-Prod"
  - tag: "Cluster[1..40]"
  - process-group-name:
    - 1: "cluster_prod1-john"
    - 2: "cluster_prod2-mike"
    - 3: "cluster_prod3-tom"
    - 4: "cluster_prod4-michael"

Integration tests are failing if executed in parallel

Test are failing due to kubernetes credentials requiring unique endpointUrl. If credentials defined in cmd/monaco/test-resources/integration-all-configs/project/kubernetes-credentials/kubernetes.json are deployed once and (still) not cleaned, tests are going to fail.

Support to add additional APIs by specifying them via program argument

We need to have a mechanism for specifying additional APIs which are present at Dynatrace, but which we haven't included in the api.go currently. api.go contains the APIs which are already well-tested. However, there are a lot of other APIs which we didn't need before - so we never added them to the tooling.

In order to support these APIs, we want to perform the following actions:

  • add a program argument for specifying a path to a yaml file
  • the yaml file is a simple list, which specifies is in the following format:
apis:
  - some-new-api: path-to-api
  - some-new-api-2: path-to-api-2
  • these APIs should be usable in addition to the one listes in api.go

Integration tests fail because of unknown synthetic test location

Describe the bug
I recently switched out the environments used for the integration tests. Now the used synthetic location in one of the tests is not correct anymore. This causes the build to fail.

How to reproduce
Simply run the integration tests

Expected behavior
The integration tests should work again.

Log output

2020-11-25 08:50:20 ERROR Deployment to environment1 failed with error Failed to upsert DT object Federation Availability_1606294218698578794AllConfigs (HTTP 400)!
    Response was: {"error":{"code":400,"message":"Unknown location(s): [GEOLOCATION-DD9455EF49BF2F89]"}}, responsible config: test-resources/integration-all-configs/project/synthetic-monitor/availabilty.json
2020-11-25 08:50:20 INFO  Deleting 19 configs for environment environment1...
    all_configs_integration_test.go:44: assertion failed: -1 (statusCode int) != 0 (int)

Environment (please complete the following information):

  • OS: All

Providing general information to template

At the moment only configuration parameters are applied to templates, but it would be good to have more information there:

  • environment object (e.g. environment url name or group)
  • project name
    ....

Improve the documentation for env vars: clarify why the env-var-name property shouldn't be used with {{ .Env.X }}

See the following comment here:

Because the `env-token-name` property is implemented in a way to serve as a usability improvement. The actual token is read from the environment variable as soon as something is applied to an environment and not before that. This ensures, that:
* a dry-run doesn't need any tokens
* you only need to provide the token for the environment you want to apply the configs to

E.g.: in your `environments.yaml` you defined 2 environments (dev and prod) and you just want to apply the configs to dev. To do that, you don't need to have the token for prod set as an environment variable.

How could we handle upload of secrets via the tool?

There are several Configurations that rely on secrets - e.g. AWS credentials, kubernetes credentials, secret vault secrets for synthetic tests, ... - but the tool currently has no way to deal with secrets in a secure way.

Revisit configuration deletion

Deletion of configuration is currently defined on project folder root level.

However, for a customer's perspective, it would make sense to allow project-specific deletion as well. A project for a customer could be a particular application (or group of) and these configurations are often handled separately from other projects. For example: I only want to delete a request attribute related to project (= customer application) X.

Download current environment configuration

For initial work with Dynatrace Monitoring as Code, customers would like to be able to download all the current configuration into a project structure.

For example, when running MaC with a flag (e.g.: download), all the supported configurations are downloaded and stored in a new project folder with the entire structure in place, as well as an initial .yaml file to apply. Optionally, the user can specify a list of configuration types that they want to download (e.g.: if they only want management-zones and auto-tag).

This will help existing customers to get started with MaC and start migrating their existing config over.

Slashes can not be used in variables - Caused by current OS path-seperator handling

Describe the bug
Monaco fails for synthetic tests when a variable uses special characters. However passing the same string directly via JSON works.

How to reproduce

my_config:
  - ua_string: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"

and

{
...
  "script": {
    "configuration": {
      "userAgent": "{{ .ua_string }}"
    }
  }
...
}

Fails with (line 11 is the userAgent line):

Response was: {"error":{"code":400,"message":"Could not map JSON at 'script' near line 11 column 20"}}, responsible config: projects\baseconfig\synthetic-monitor\myfile.json

Whereas removing that YAML variable and pushing this, works just fine.

{
...
  "script": {
    "configuration": {
      "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
    }
  }
...
}

How do we want auto-filling of Service (and other?) meIds to work

Having to fill MeIDs into e.g. dashboard configs is tedious.

We'd like to automate that and allow to just reference other configs and have a dashboard auto-filled.

However MeIDs of e.g. a Service do not map to the service detection rule ID, and aren't even available until data is first picked up.

How would we want the tool to handle this?

  • Fail until IDs are available?
  • Ignore the error until later deployments?
  • Wait during deployment until IDs are available?
    ....

Integration Tests Failing on Unexpected Return Values from Dynatrace

Describe the bug
Our integration tests fail.

How to reproduce
Run integration tests against a current SaaS production environment.

Expected behavior
Integration tests work.

Log output

=== RUN   TestDoCleanup
2020-11-23 07:41:36 ERROR Cannot unmarshal API response for existing objects: invalid character '<' looking for beginning of value
    cleanup_all_configs_test.go:50: assertion failed: error is not nil: invalid character '<' looking for beginning of value
--- FAIL: TestDoCleanup (3.86s)
FAIL
FAIL	github.com/dynatrace-oss/dynatrace-monitoring-as-code/cmd/monaco	3.863s
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/api	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/config	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/delete	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/environment	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/project	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/rest	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/util	[no test files]
?   	github.com/dynatrace-oss/dynatrace-monitoring-as-code/pkg/version	[no test files]
FAIL
make: *** [integration-test] Error 1
Makefile:25: recipe for target 'integration-test' failed
Error: Process completed with exit code 2.

Environment (please complete the following information):

  • OS: github build env ubuntu-latest
  • Tool version 1.0.0 & 1.0.1

[Discussion] Public communication channel

Having open sourced our work, we want to open our communication channels as well.
That means moving away from our internal Slack channels to something that external contributors [0] can use as well.

Let's use this to discuss options!

A few ideas:
Slack - pro: extra workspace can just be opened in Slack apps we already use internally all the time, con: text only
Discord - pro: widely used, has voice chat, con: extra tool

Decision Deadline: 2020-12-12
Participation Required: @didiladi , @tatomir146 , @CruzanCaramele

[0] e.g. @goberchtold (testing if reaching out this way works)

Fix documentation: -- or -

Readme suggests the flags use double dashes. For example --dry-run but the v1.0.0 binary still uses single dashes eg. -dry-run

Instructions on how to build locally are not fully correct

The CONTRIBUTING.MD file says that go build ./... should work fine but it seems that this is not correct. Instead, being in the project's root directory you need to run go build ./cmd/... it seems and then it spits out the monaco binary.

If I'm not overlooking anything then this should be fixed in the documentation. If no one has any more input on this I'd gladly adjust the documentation and create a PR for it :)

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.