GithubHelp home page GithubHelp logo

trace4cats / trace4cats Goto Github PK

View Code? Open in Web Editor NEW
184.0 7.0 34.0 2.79 MB

Distributed app tracing implementation in pure scala using cats-effect

License: MIT License

Scala 100.00%
scala cats tracing jaeger opentracing opentelemetry opencensus cats-effect native-image graalvm

trace4cats's Introduction

Trace4Cats

GitHub Workflow Status GitHub stable release GitHub latest release Maven Central early release Join the chat at https://gitter.im/trace4cats/community Scala Steward badge

⚠️ If you are upgrading to 0.14.0 please read the migration guide.

Yet another distributed tracing system, this time just for Scala. Heavily relies upon Cats and Cats Effect.

Compatible with OpenTelemetry and Jaeger, based on, and interoperates with Natchez.

Obligatory XKCD

For release information and changes see the releases page.

Motivation

It increasingly seems that Java tracing libraries are dependent on gRPC, which usually brings along lots of other dependencies. You may find Trace4Cats useful if you want to...

  • Reduce the number of dependencies in your application
  • Resolve a dependency conflict caused by a tracing implementation
  • Create a native-image using GraalVM

Highlights

Trace4Cats supports publishing spans to the following systems:

Instrumentation for trace propagation and continuation is available for the following libraries:

Unlike other tracing libraries, trace attributes are lazily evaluated. If a span is not sampled, no computation associated with calculating attribute values will be performed.

More information on how to use these can be found in the examples documentation.

Quickstart

For more see the documentation and more advanced examples.

Add the following dependencies to your build.sbt:

"io.janstenpickle" %% "trace4cats-core" % "0.14.0"
"io.janstenpickle" %% "trace4cats-avro-exporter" % "0.14.0"

Then run the collector in span logging mode:

echo "log-spans: true" > /tmp/collector.yaml
docker run -p7777:7777 -p7777:7777/udp -it \
  -v /tmp/collector.yaml:/tmp/collector.yaml \
  janstenpickle/trace4cats-collector-lite:0.14.0 \
  --config-file=/tmp/collector.yaml

Finally, run the following code to export some spans to the collector:

import cats.Monad
import cats.data.Kleisli
import cats.effect._
import cats.effect.std.Console
import cats.implicits._
import trace4cats._
import trace4cats.avro.AvroSpanCompleter

import scala.concurrent.duration._

object Trace4CatsQuickStart extends IOApp.Simple {
  def entryPoint[F[_]: Async](process: TraceProcess): Resource[F, EntryPoint[F]] =
    AvroSpanCompleter.udp[F](process, config = CompleterConfig(batchTimeout = 50.millis)).map { completer =>
      EntryPoint[F](SpanSampler.always[F], completer)
    }

  def runF[F[_]: Monad: Console: Trace]: F[Unit] =
    for {
      _ <- Trace[F].span("span1")(Console[F].println("trace this operation"))
      _ <- Trace[F].span("span2", SpanKind.Client)(Console[F].println("send some request"))
      _ <- Trace[F].span("span3", SpanKind.Client)(
        Trace[F].putAll("attribute1" -> "test", "attribute2" -> 200) >>
          Trace[F].setStatus(SpanStatus.Cancelled)
      )
    } yield ()

  def run: IO[Unit] =
    entryPoint[IO](TraceProcess("trace4cats")).use { ep =>
      ep.root("this is the root span").use { span =>
        runF[Kleisli[IO, Span[IO], *]].run(span)
      }
    }
}

Migrating to 0.14.0

Version 0.14.0 introduced a reworked module and package structure that reduced the number of dependencies and imports required to get started quickly. Effectively import trace4cats._ is all you should need to import throughout most of your codebase.

See the migration guide for information on how to migrate.

Repositories

Trace4Cats is separated into a few repositories:

Components

Trace4Cats is made up as both a set of libraries for integration in applications and standalone processes. For information on the libraries and interfaces see the design documentation.

The standalone components are the agent and the collector. To see how they work together, see the topologies documentation, for information on configuring and running the agent and collector see the components documentation.

The source code for these components is located in the trace4cats-components repository.

Documentation

SBT Dependencies

To use Trace4Cats within your application add the dependencies listed below as needed:

"io.janstenpickle" %% "trace4cats-core" % "0.14.0"
"io.janstenpickle" %% "trace4cats-rate-sampling" % "0.14.0"
"io.janstenpickle" %% "trace4cats-fs2" % "0.14.0"
"io.janstenpickle" %% "trace4cats-http4s-client" % "0.14.0"
"io.janstenpickle" %% "trace4cats-http4s-server" % "0.14.0"
"io.janstenpickle" %% "trace4cats-sttp-client3" % "0.14.0"
"io.janstenpickle" %% "trace4cats-sttp-tapir" % "0.14.0"
"io.janstenpickle" %% "trace4cats-natchez" % "0.14.0"
"io.janstenpickle" %% "trace4cats-avro-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-avro-kafka-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-avro-kafka-consumer" % "0.14.0"
"io.janstenpickle" %% "trace4cats-jaeger-thrift-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-opentelemetry-otlp-grpc-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-opentelemetry-otlp-http-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-opentelemetry-jaeger-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-stackdriver-grpc-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-stackdriver-http-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-datadog-http-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-newrelic-http-exporter" % "0.14.0"
"io.janstenpickle" %% "trace4cats-zipkin-http-exporter" % "0.14.0"

native-image Compatibility

The following span completers have been found to be compatible with native-image:

Contributing

This project supports the Scala Code of Conduct and aims that its channels (mailing list, Gitter, github, etc.) to be welcoming environments for everyone.

trace4cats's People

Contributors

alejandrohdezma avatar bplommer avatar catostrophe avatar cremboc avatar dependabot[bot] avatar desbo avatar hygt avatar janstenpickle avatar kovstas avatar mergify[bot] avatar mrdziuban avatar scala-steward avatar sideeffffect avatar soujiro32167 avatar tarmath avatar timbess avatar trace4cats-steward[bot] avatar ybasket 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

trace4cats's Issues

Open Telemetry exporters not setting service name

Open Telemetry exporters are setting an empty resource but setting a service name on every span, which used to display fine in Jaeager, unfortunately this is no longer the case. It is now required that service.name is set on the resource in a span batch.

Case sensitivity in b3 headers

I am trying to see a call flow from Envoy to a backend that uses trace4cats. The incoming headers include b3 headers, like this:

image

However, in Jaeger UI, I cannot see the connection between the parent trace from Envoy and the child created by trace4cats.
It looks like here the headers are compared directly, with case sensitivity, whereas the protocol allows lower case headers:

https://github.com/openzipkin/b3-propagation

Scalafix migrations produce invalid imports

Glad to see the Scala Steward PRs for 0.14!

Noticed one small issue though, the imports sometimes get messed up with trace4cats being concatenated several times:
Screenshot 2022-08-01 at 13 14 20
Screenshot 2022-08-01 at 13 14 40

Of course users can resolve that manually, but maybe there's a quick fix we can apply to make their lives easier?

[http4s] Drop/redact sensitive headers before putting them into the span

All methods in this object io.janstenpickle.trace4cats.http4s.common.Http4sHeaders must accept an extra argument, e.g.:
dropHeadersWhen: CaseInsensitiveString => Boolean = Headers.SensitiveHeaders.contains (and hence all the methods using it transitively).

Sensitive headers must not be exposed. I think we could just drop them rather than redact.

Generalize trace injectors and lifts

The provided integrations are not general: they only support contextual effects in forms of ReadertT[F, Span[F], *] or ZIO[Span[Task], Throwable, A]. But in real-life use cases, the main program effect usually has a more complex compound evaluation context, which contains Span[F] and may contain auth, logging ctx, dependencies, etc.

I suggest modifying the provided integrations and making them for abstract Ctx. I have a POC in my private codebase. I use context typeclasses and optics from tofu. But it may be done using trace4cats own typeclasses as @janstenpickle has already created (needs some improvements).

Preparation for v0.13.0

3 months have passed since 0.12.0. There have been not many changes in t4c libs since then, but a lot of dependencies have been bumped. I think it's time to close some issues and cut 0.13.0 just to show the users we keep t4c up-to-date 😃

If anyone has ideas about what else can be improved before the new release, please open an issue.

Below is the list of issues that should be fixed before the release:

Issues that may be fixed:

Issues related to trace4cats-components:

Issues related to flaky tests (low-priority):

Other issues (low-priority):

Bug: Duplicate span IDs

When testing #729 with my demo repo, I encountered many duplicate span IDs within a trace. It seems as #732 introduced a regression here when switching to cats.effect.std.Random – the instance acquired via Random.javaUtilConcurrentThreadLocalRandom seems to start with the same seed (which can't be changed) for each of CE's worker threads, resulting in similar span ID sequences being produced. This isn't well-documented, but quite visible in tests (maybe one should be added to trace4cats to check for this?). If one provides a Random instance backed another RNG implementation in implicit scope, the problem vanishes. Probably affects trace ID generation as well.

One can test the problem with this simple app:

import cats.effect._
import cats.effect.std.Random
import cats.syntax.all._
import io.janstenpickle.trace4cats.model.SpanId

object Randoms extends IOApp.Simple {
  override def run: IO[Unit] = (spans, spans, spans, spans).parMapN { (b1, b2, b3, b4) =>
    IO.println((b1, b2, b3, b4))
  }.flatten

  val spans = SpanId.gen[IO].replicateA(8)

  val bytesTL = IO(Random.javaUtilConcurrentThreadLocalRandom[IO].nextBytes(8).map(SpanId.unsafe).replicateA(8)).flatten
  val bytesSR = Random.scalaUtilRandom[IO].flatMap(_.nextBytes(8).map(SpanId.unsafe).replicateA(8))
  val bytesSRN = Random.scalaUtilRandomN[IO](8).flatMap(_.nextBytes(8).map(SpanId.unsafe).replicateA(8))
}

It'll print something like

(List(5332f38d1b42bce2, 5332f38d1b42bce2, 5332f38d1b42bce2, 5332f38d1b42bce2, 5332f38d1b42bce2, 5332f38d1b42bce2, 5332f38d1b42bce2, 5332f38d1b42bce2),List(24329fc1655fc619, 24329fc1655fc619, 24329fc1655fc619, 24329fc1655fc619, 24329fc1655fc619, 24329fc1655fc619, 24329fc1655fc619, 24329fc1655fc619),List(9880c7fbd03a316e, 9880c7fbd03a316e, 9880c7fbd03a316e, 9880c7fbd03a316e, 9880c7fbd03a316e, 9880c7fbd03a316e, 9880c7fbd03a316e, 9880c7fbd03a316e),List(1bb4e609932ac0a5, 1bb4e609932ac0a5, 1bb4e609932ac0a5, 1bb4e609932ac0a5, 1bb4e609932ac0a5, 1bb4e609932ac0a5, 1bb4e609932ac0a5, 1bb4e609932ac0a5))

By replacing spans with one of the bytesXYZ, you can see the behaviours of different RNGs.

Unfortunately, the issue isn't super-easy to fix because all Random constructors except javaUtilConcurrentThreadLocalRandom are effectful, so either the user needs to always provide a Random instance (scalaUtilRandomN), the instance is created via SyncIO and then mapKed (a hack) or cats.effect.std.Random is removed from the Gen traits (effectively partially reverting #732).

@catostrophe

Drop better-monadic-for plugin

This is a blocker for migration to Scala 3. Not an urgent thing but something we definitely should do sooner or later. Currently, it's used mostly for passing implicit loggers and such pieces of code can be easily rewritten without bm4.

Wrong SpanRefType mapping in JaegerSpanExporter

I think there is an error in the mapping between the internal trace4cats span model and the Jaeger model.

https://github.com/janstenpickle/trace4cats/blob/82baa78bd7ee1c5e7742d99d4f53477a43bd76f7/modules/jaeger-thrift-exporter/src/main/scala/io/janstenpickle/trace4cats/jaeger/JaegerSpanExporter.scala#L76-L77

Here's the link to an interesting discussion regarding their meaning:
open-telemetry/opentelemetry-specification#65

They both mean "parent" or "ancestor", not a child, but in a bit different way.

The below quote is especially interesting:

Another odd thing about child-of and follows-from is that it's the child span that defines this reference type, even though it talks about parent's dependency on child outcome. If you're a remote server, how do you even know if parent/caller does or does not depend on your outcome? I tend to think of this as the nature of the protocol: producer of a message to Kafka does not respect any response, so the receiver should use follows-from. Sender of HTTP request does expect a response, so the server always uses child-of, even of the sender doesn't care about the outcome - in that case it can internally create a follows-from span first, and then a normal pair of RPC call spans. So it's possible to rationalize this way, but it's still kind of dirty.

I suggest changing it to:

case Link.Parent(_, _) => new SpanRef(SpanRefType.CHILD_OF, traceIdLow, traceIdHigh, spanId) 

That is when we add a link Parent(parentTraceId, parentSpanId) we mean that the current span is a child of (parentTraceId, parentSpanId).

X-B3-TraceId support for 8 bytes size as well as 16 bytes size

When trying to use ToHeaders.B3 , which is based on the X-B3-TraceId headers, the code is assuming that the traceId size must be 16 bytes.

The current X-B3-TraceId spec, clearly says that both 8 bytes and 16 bytes size are valid: https://github.com/openzipkin/b3-propagation#traceid

As a workaround, I tried creating my own ToHeaders but I was unable to do so, as the TraceId case class constructor is private. There's an apply Method: apply: Option[TraceId] that will give you None if the size is not 16

def apply(array: Array[Byte]): Option[TraceId] =
    if (array.length == size) Some(new TraceId(array)) else None

where size is initialized at 16 on the TraceId object

I think that

private[trace4cats] class B3ToHeaders extends ToHeaders {

implementation should allow using TraceId of 8 bytes as well of 16 bytes

Reconfigure automatic early releases

Currently, we skip release for PRs merged by the mergify bot. It is not enough as mergify can't merge changes into .github dir.
When I merge such changes myself, it causes a redundant release that just spoils Maven Central, so I stop the build.

I would like to be able to cut an early release when needed (e.g. when we want to test a new feature), but to skip it for upgrades in workflows.

I don't know how, though :) Any ideas?

Finer grained span timing

Firstly, thanks for the great library 🙂

One thing I've noticed is that the spans are at millisecond granularity. Was this a deliberate decision? It seems like it would be useful to trace at microsecond granularity to facilitate fine grained tracing of function calls, etc.

New Arbitrary generators cause flaky tests in other repos

New instances in ArbitraryAttributeValues use an unrefined Double generator. This causes very often failures in tests in trace4cats-opentelemetry.

I suggest changing the double generator to avoid floating-point positions that may cause formatting of the result in the scientific format, i.e. 0.00000123 -> 1.23E-6.

Do we lose anything with this change?

cc @ybasket @janstenpickle

Customize `SpanContext` on root span creation

Hello there,

I have this use case where my company have a legacy "logging id" (which is passed around via HTTP headers) as well as the regular B3 traces. I'd like to store this logging id in a SpanContext (to be able to pass it around, as well as include it in logs). I can do that relatively easily by modifying the concrete span implementation once it has been created by an EntryPoint. The issue I'm facing is that the original span (the one created by Span.root or Span.child, and not modified) is being sent to the SpanCompleter.
My idea was to create a new EntryPoint that would modify the SpanContext before creating the actual Span. That works well for a child span, because we have to explicitly pass the parent's context but for root spans the SpanContext is created automatically and cannot be modified.

All this context lead to my question: would it be possible to make the root SpanContext customizable? Perhaps by passing the SpanContext as a parameter (with SpanContext.root[F] as the default) ?

If there is an agreement, I can open a PR with this change (or any other that are deemed preferable)

The need for no-op entities

In order to make tracing optional (switchable) at the application level, e.g. via config, we need to be able to create no-op instances of some entities.

We have one for Trace. But the one for EntryPoint is missing. As to implement it, we need a NoopSpan which in turn cannot be created without the empty SpanContext.

I will add those listed above if we define what the empty SpanContext is.

Sttp integration module needs reorganization

I suggest some changes:

  • rename backend package to client as it's named in sttp
  • consider support for sttp.client3
  • add integration with tapir: ServerEndpoint may be span injected (in many cases it is preferable over routes tracing)
  • add common package for common code used for sttp client and tapir integrations

Simplified module/package structure

When trialling t4c at $work I've had a lot of complaints about the number of different packages and modules that are needed for the core functionality. Here are a few tentative suggestions for reducing barriers to adoption:

modules

  • combine model into kernel
  • combine exporter-common, base and inject into core

packages

  • move contents of model and inject packages into either the base package or trace and span packages
  • move contents of kernel package into either the base package or a span package
  • consider changing the base package from io.janstenpickle.trace4cats to just trace4cats

Any thoughts on this?

Reworked package structure

Follows from #701. Let's discuss a new package name and structure.

  • As suggested previously, we should probably go with trace4cats as the root package
  • We'll need to provide Scalafix migrations for ScalaSteward - I've not done this before, so any help would be appreciated!
  • As part of #701 and #742 some libraries will no longer be published (inject probably being the most used), we'll need to leave deprecated versions of interfaces to redirect to the new libraries and packages

Let's discuss!

Inverted meaning for `Traceflags#sampled`

Looks like project uses inverted value for the sampled flag.

In W3C Trace Context true/01 means "the caller may have recorded trace data", but in SpanSampler#shouldSample/Span true means "don't report the span". The latter is consistent across the project, except for ToHeaders.w3c, which passes the flag as-is.

I may be misunderstanding flag semantics, but wouldn't that effectively disable child traces?

Docker images don't get pushed for stable release versions

These steps are skipped:

  PAT='^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$'
  if [[ $VERSION =~ $PAT ]]; then
    docker tag janstenpickle/trace4cats-collector:latest janstenpickle/trace4cats-collector:$VERSION
    docker push janstenpickle/trace4cats-collector:$VERSION
  else
    exit 0
  fi

The latest release image is 0.7.0

Introduce a newtype for headers map used in EntryPoint and ToHeaders

I would like to have a newtype wrapping Map[String, String] for headers being passed to EntryPoint's and ToHeaders's methods as it's done in Natchez.

Something simple like the following would be fine:

final case class TraceHeaders(toMap: Map[String, String]) extends AnyVal

I would do it myself, if you're ok with the naming. Which module should it be put into. I suppose core?

The motivation is that in my codebase I write custom span injectors and I require optics for abstract type parameters, which parameterize endpoint inputs or evaluation context, and for optics, especially implicit ones, it is highly desirable to have newtypes. That is Getter[Ctx, TraceHeaders] is better than Getter[Ctx, Map[String, String]].

I use it in my code, but I think it also may be useful in the library.

Break up project

Trace4cats project has become very large. Builds are taking a long time and non-core dependencies are sometimes delaying the release of new versions (e.g. CE3/Tapir).

I propose creating a new organisation and the following repositories (I think I've covered everything, if it's not listed below it's probably captured by the core repo):

  • trace4cats
    • core libraries and interfaces
  • trace4cats-zio
    • Zio implementations of core interfaces
  • natchez
    • Conversion to/from Natchez traces
  • trace-http4s
    • trace context injectors for http4s interfaces
  • trace-sttp
    • context injectors for sttp/Tapir interfaces
  • trace-kafka
    • context injectors for Kafka clients
  • http-exporter
    • common code for constructing http based exporters
  • avro
    • Avro exporters; TCP, UDP and Kafka. (We may want an avro-kafka module, just to further separate concerns)
  • opentelemetry
    • Open Telemetry export implementations
  • jaeger
    • Jaeger export implmentations
  • stackdriver (Maybe rename to google-cloud-trace)
    • Stackdriver export implmentations
  • datadog
    • Datadog exporter
  • newrelic
    • NewRelic exporteer
  • zipkin
    • Zipkin exporter
  • tail-sampling
    • Tail sampling interfaces & implementations
  • components
    • the collectors and agents
  • docs
    • Documentation and examples

Obviosuly this makes ongoing management a bit more difficult, but hopefully scalasteward, depandabot and mergify will help there.

The only question is timing, I think we should maybe aim to do this prior to full Scala 3 migration so that lagging dependencies can be updated as they become available.

A good first step may be to separate out the components repo so the build is separated and #515 fixed.

`Trace` implementations written directly against `Kleisli` and `IOLocal`

Having everything implemented against the contextual abstractions is elegant conceputally, but it also creates an obstacle to understanding the library (you need to internalise the abstractions and/or follow extra indirection) - providing specific concrete implementations would make it easier to understand the library in terms of concrete behaviour, while also providing an on-ramp for understanding the more abstract implementation.

Some urls in the README need to be changed.

Hello,
I noticed some of the urls in the README are broken, specifically the links under the ##Components section. I'll open a PR to fix this and update the issue with the PR #.

ClientSyntax/RoutesSyntax: issues with inject

There is some issues with inject implementation for both client and server:

  • Span#setStatus isn't called on error or cancel - probably, bracket-like method should be used instead of flatMap?
  • RoutesSyntax#inject operates on HttpRoutes, ignoring "not found"/None case - one option would be to translate HttpApp, which already have this case converted to 404;
  • Client inject uses Client#toHttpApp - that would cause connection leak in some setups if HTTP body isn't consumed (for example, if a client don't read it on 4xx errors).

Please correct me if my view is incomplete or if I'm missing something here.

Migrate to CE3

This issue is to track dependencies and blockers.

CE3 tracking issue: typelevel/cats-effect#1330

PR with CE3 changes: #374

Dependencies affected by CE3 migration:

  • cats-effect - released v3.1.0 - ok 🟢
  • fs2 - released v3.0.2 - ok 🟢
  • log4cats - released v2.1.0 - ok 🟢
  • natchez - released v0.1.2 - ok 🟢
  • decline - released v2.0.0 - ok 🟢
  • zio-interop-cats - released v3.0.2.0 - ok 🟢
  • sttp-client3 - released v3.3.2 - ok 🟢
  • fs2-kafka - candidate v2.0.0 - ok 🟢
  • redis4cats - candidate v1.0.0-RC3 - pending 🟡
  • http4s - milestone v1.0.0-M21 - pending 🟡
  • http4s-jdk-http-client - milestone v0.5.0-M4 - pending 🟡
  • tapir-http4s-server - WIP softwaremill/tapir#1154 - blocker 🔴

Traced logging?

Hi there! This library is awesome, and I really enjoy using it. One thing that is missing is a traced logger, similar to the one in natchez-extras (TracedLogger), where the current trace context is added to logs as mdc keys. I was curious if the maintainers would be interested in this functionality, and if so, I'd be happy to submit a PR. Thanks!

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.