open-telemetry / opentelemetry-java

OpenTelemetry Java SDK
https://opentelemetry.io
Apache License 2.0
2.02k stars 839 forks source link

remove grpc-context dependency from the api package #575

Closed codefromthecrypt closed 4 years ago

codefromthecrypt commented 5 years ago

I understand why opentelemetry has a bias towards google's gRPC library. This codebase primarily borrows from work that started internally, became google's instrumentation-java, later renamed to opencensus, and now mostly copy/pasted from that. The original primary customer was not just gRPC protocol, but specifically google's library. For example, gRPC is the most primary user of the prior work, and it is helpful to have a user to guide.

Time has passed, though, and what started as an end-to-end instrumentation library has split into a Api + SDK approach, as facilitated by the weaving in of OpenTracing. Yet, code opinons towards google's gRPC implementation still exist in the form of a strict dependency in the api package, and helper code.

While it is understandable that alliances exists, and having good integration with Google's gRPC implementation is important, even gRPC itself has options. For example, Armeria has gRPC protocol support without the need of Google's library. We have already encountered problems where other google projects drag in gRPC because of Census. This should stop now. OpenTelemetry as a body of people, sure that can advocate for Google. However, the instrumentation api must not pick winners and losers in RPC frameworks.

Please remove all traces of grpc-context from the api library and move the support functions to a context integration package.

This is a follow-up from here: https://github.com/open-telemetry/opentelemetry-java/pull/571#issuecomment-536193532

mattnworb commented 4 years ago

Hi, as a user of both gRPC and OpenCensus, there is some level of convenience in having a single "Context" class/instance I can use to pass the (OpenCensus) trace and (gRPC) request context explicitly from method to method.

I certainly understand that being exposed to a class named io.grpc.Context is confusing for applications that do not use gRPC, but if OpenTelemetry were to create its own Context-like class with a different name, for codebases already "bought in" to io.grpc.Context then having a second Context class to explicitly propagate from method-to-method and thread-to-thread would add friction as well. For "microservice"-like Java applications that use multiple threads to handle processing of a request (at least those that are exposing gRPC servers), I have found it unworkable to use implicit propagation with ThreadLocals, so a second OpenTelemetry-specific Context class would have to be passed explicitly as well (or would force developers to define their own value class holding multiple Context implementations to have a single variable to pass).

I can't help but notice that opentelemetry-go can easily refer to context.Context from Go's standard library in its Tracer API, which I think speaks to the usefulness of having a commonly used Context implementation/interface across libraries and RPC frameworks. It'd be awesome if OpenTelemetry, gRPC, and other projects could find a way to share a common Context definition with a minimal API surface (so that it changes rarely), since it seems unlikely for anything to be added to the JDK.

FroMage commented 4 years ago

Hi,

I work on MicroProfile Context Propagation (https://download.eclipse.org/microprofile/microprofile-context-propagation-1.0/microprofile-context-propagation.html) and noticed this issue which touches on a lot of topics we are concerned with, so I thought perhaps I can shed some light on what we do about context propagation.

First, unlike gRPC Context, MicroProfile Context Propagation (MP-CP for short) does not provide storage: it doesn't give you a context class where you can store your context info (type-safe or not). It doesn't do this because it's trivial to implement using a class with a ThreadLocal (or equivalent), a few methods for get/set and lifecycle. And also because a lot of existing libraries have existing context classes that they want to keep, or their lifecycle is complicated, or they have custom requirements for serialising it.

What MP-CP does however, is propagate any number of pluggable contexts without caring how they're defined by the frameworks that use them. In short, if your framework has a context class, you only have to implement a ThreadContextProvider interface and provide it with methods for:

And once you do that, all applications that use context propagation will have your context propagated.

The propagation is really the hard part, because many applications support either async parallel methods that spawn a request (if your framework is request-based) into offshoots of the request processing that run on separate threads or executors, none of which would normally see the ThreadLocal where frameworks store their contexts. Or in the case of reactive programming, there's going to be a lot of callbacks executed on whatever current thread calls them, or executors, which again in both cases will have no ThreadLocal or worse: the wrong one.

What MP-CP does is that it allows applications that want contexts to be propagated to wrap their callbacks into ones that capture the current context on creation, and restore it for the duration of its execution. There's context propagation for common callbacks such as Runnable, Function, Supplier but also CompletionStage and CompletableFuture.

We also have a plugin mechanism which allows us to hook automatic context propagation for reactive libraries such as RxJava or Mutiny, which means the user doesn't have to change their code at all to get context propagation automatically and intuitively.

As far as I'm aware, MP-CP is the only solution which provides pluggable context propagation without forcing you to handle the propagation yourself, or to change your context storage solution, or to change your underlying reactive API.

Given your requirements, I think you could just define your own storage class, which can have type-safe keys if you need that, and custom serialisation if you send this over the wire, and as long as you allow access to getting and setting it, you can (or someone else can) implement a plugin for MP-CP that will make it propagated for everyone using MP-CP for propagation.

ATM MP-CP only defines interfaces and depends only on MP-Config so it's a fairly small dependency, and you don't need to depend on an implementation (such as SmallRye Context Propagation). You could split your ThreadContextProvider into a new module just containing that class if you want to avoid the dependency, and people who want propagation of your context would import it. Or you can put that class in your base module and everyone using context propagation would automatically get your context propagated when they import your module.

It's not clear to me whether you need an implementation of context propagation in your module, however, given that you currently use gRPC context which doesn't do propagation (in process), so I think this is not a requirement for you ATM.

Hope that helps, and let me know if you have any question. Cheers.

iNikem commented 4 years ago

@FroMage This is very interesting for us in auto instrumentation for Java. Can you please take a look at this use-case? How does MP-CP handle such "run-away" contexts?

jkwatson commented 4 years ago

@FroMage Super interesting. I'm going to set aside from time to read the spec document. Thanks for linking this here!

sjoerdtalsma commented 4 years ago

@FroMage thanks for the link. It sounds like a perfect substitute for my hand-rolled api. I'll definitely take a look at it.

FroMage commented 4 years ago

Can you please take a look at this use-case? How does MP-CP handle such "run-away" contexts?

You can either inject a ManagedExecutor which clears all contexts for async actions if the context is meant to be closed before they terminate, or if the context is meant to be kept open and used until all such async actions are completed, then it's up to your application to keep track of when those async actions terminate before you can close your context.

jkwatson commented 4 years ago

@FroMage Is Microprofile Context Propagation locked to java 8+, or could it be used with java 7?

carlosalberto commented 4 years ago

Assigning this to @malafeev as he will do an initial digging & comparison of MicroProfile and grpc's Context.

malafeev commented 4 years ago

Java 7 is not supported by MP-CP API. It's based on Java 8 features. When I tried to compile

 ManagedExecutor executor = ManagedExecutor.builder()
        .maxAsync(5)
        .propagated(ThreadContext.CDI, ThreadContext.APPLICATION)
        .build();

    ThreadContext threadContext = ThreadContext.builder()
        .propagated(ThreadContext.SECURITY)
        .build();

I got

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project test: Compilation failure: Compilation failure: 
[ERROR] /Users/malafes/projects/test/src/main/java/Main.java:[8,47] static interface method invocations are not supported in -source 1.7
[ERROR]   (use -source 8 or higher to enable static interface method invocations)
[ERROR] /Users/malafes/projects/test/src/main/java/Main.java:[13,48] static interface method invocations are not supported in -source 1.7
[ERROR]   (use -source 8 or higher to enable static interface method invocations)
FroMage commented 4 years ago

Yeah, it's JDK8+

codefromthecrypt commented 4 years ago

I would summarize this as another key consumer/integration besides grpc-context. There is also https://github.com/alibaba/transmittable-thread-local which focuses on some of this even if we formerly mostly discussed RPC and reactive.

The main idea here was keeping the deps clear and knowing that while solving for gRPC is important, so is making other things possible, which share different dependency characteristics, etc.

malafeev commented 4 years ago

from my MP CP review:

FroMage commented 4 years ago

MP CP is not a solution which we can take and use without doing own implementation of interfaces.

Well, there's SmallRye-ContextPropagation which implements it that you can use. I don't see the point in re-implementing it.

MP CP is coupled to Executors. Threads should be managed by container...

That's only the case for ManagedExecutor but not for ThreadContext which is what you will want to use.

MP CP is java 8+. It’s based on java 8 concurrency api: completable future is a first citizen.

Yes, that's the case.

MP CP must to be compatible with Java EE Concurrency model/api, therefore it may have unnecessary functionality.

This is only true of ManagedExecutor: a single class.

I was not able to create an example to propagate value like a string from one thread to another thread reading documentation. So it’s still a black box for me.

Do you want me to provide a complete example here?

malafeev commented 4 years ago

Do you want me to provide a complete example here?

@FroMage it would be nice, thanks

FroMage commented 4 years ago

So, there's your test class:

package whatever;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.eclipse.microprofile.context.ThreadContext;
import org.eclipse.microprofile.context.spi.ContextManager;
import org.eclipse.microprofile.context.spi.ContextManagerProvider;
import org.junit.Assert;
import org.junit.Test;

public class DemoTest {
    @Test
    public void test() throws InterruptedException, ExecutionException {
        ContextManager contextManager = ContextManagerProvider.instance().getContextManager();
        ThreadContext threadContext = contextManager.newThreadContextBuilder().build();

        ExecutorService executorService = Executors.newFixedThreadPool(2);
        MyContext.clear();
        // start with no context
        Assert.assertNull(MyContext.get());

        // verify that new threads also get no context
        executorService.submit(() -> {
            Assert.assertNull(MyContext.get());
        }).get();

        // now set the context
        MyContext ctx = new MyContext();
        MyContext.set(ctx);
        // verify that we have a context
        Assert.assertEquals(ctx, MyContext.get());

        // verify that new threads still get no context
        executorService.submit(() -> {
            Assert.assertNull(MyContext.get());
        }).get();

        // verify that new threads with context propagation do get context
        executorService.submit(threadContext.contextualRunnable(() -> {
            Assert.assertEquals(ctx, MyContext.get());
        })).get();
    }
}

And here's your context class:

package whatever;

public class MyContext {

    private static ThreadLocal<MyContext> context = new ThreadLocal<MyContext>();

    public static void init() {
        context.set(new MyContext());
    }

    public static void clear() {
        context.remove();
    }

    public static MyContext get() {
        return context.get();
    }

    public static void set(MyContext newContext) {
        context.set(newContext);
    }

// put your context properties here
    private String reqId;

    public void set(String reqId) {
        this.reqId = reqId;
    }

    public String getReqId() {
        return reqId;
    }
}

This is typically where you'd put whatever context values you need.

You'll need to have this src/main/resources/META-INF/services/org.eclipse.microprofile.context.spi.ThreadContextProvider file too:

whatever.MyThreadContextProvider

You'll want to use SmallRye-CP:

        <dependency>
            <groupId>io.smallrye</groupId>
            <artifactId>smallrye-context-propagation</artifactId>
            <version>1.0.16</version>
        </dependency>

That's all. Now you can both propagate any number of contexts, and make sure that your context will be propagated to any CP user.

Does that help?

jkwatson commented 4 years ago

In the 9/15[16] java meeting, it was agreed that @anuraaga will create a new module and start building our own context implementation. Thanks @anuraaga for spearheading this work!