-
Notifications
You must be signed in to change notification settings - Fork 8
Add HTTPWithContextFnOpts so requests can have eval-time context #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
efd6
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you show me how this intended to be used? I've looked at elastic/beats#48440, but I don't see that doing anything that could not be done with the API as it stands since the context that is used via the closure is not altered by the closure, meaning that any operation on the context would propagate to all other instances of the value. Is there more do be done in that change that would help me understand why this is necessary?
|
In the existing code, when HTTP requests from CEL are performed, it's like this: with context put into So requests always execute with the context from CEL program creation time. Multiple evaluations of the program will use the same old context for HTTP requests, and the OTel tracing span for the HTTP request won't be able to identify a particular evaluation as its parent. Ideally, cel-go's So my workaround is to pass in a function at program creation time, and make sure it will return the right context during each eval. In elastic/beats#48440 I pass in the function here: and make sure it'll return the right context for each eval here: When the HTTP request is performed, it'll call the function to get the current context, here: |
|
That's what I thought, but I'm not convinced it does what you are wanting.
This results in a shared context. The AFAICS for what you want to do, the context that is returned by the closure must be distinct in some way from the parent (exactly how is the thing that I'd like to understand). When you say, "return the right context during each eval", what is "the right context"? As far as the implementation goes, I'd like not to expand the API like this. Given that
I think the first of these is simpler and clearer. |
|
Thanks. That explains it, and I now see what you are doing. This complexity comes from the issues that were discussed here. I would like not to do it with the closure and instead use the optional interface approach. Apart from not expanding the API surface and with careful construction, the data flow that I missed in the closure behaviour can be made explicit. |
|
Okay, I'm trying to think through the options. I see that adding Your earlier suggestion would have that new function be provided by a new interface that is optionally implemented by the existing argument. Mito still gets expanded to handle both cases, but that's not explicit in its API. We couldn't mark the old way as deprecated because the new way is embedded in the old way. We'd have the context provider we need wrapped in a different context that we don't need. The optional interface would make more sense to me if it was extracting something from the type implementing it, and if it wasn't returning a different, unconnected instance of the same type. Keeping the existing function signature doesn't seem to be better overall. I was thinking, if |
This is important. We would need to make a v2 if it is removed.
I'm not sure what you mean by this.
I don't see this as a problem. If the interface were I'm not sure what you mean in the last paragraph. By "Mito", do you mean the |
I don't mind keeping it backwards compatible, but I think a new version would also be fine.
On the CEL Input side I have these different context values: The
I don't think it's strictly wrong, just counter intuitive. The interface can explain itself but what it's doing still seems odd to me. Usually you pass forward a context and any changes are propagated forward and you always use the current one. It seems odd to have an older context value and call a method on that to get the current one.
No, not that. |
c10264e to
b8eeb3b
Compare
v2s are a pain. I'd really rather avoid it unless it's entirely necessary, which I do not believe is the case here.
Except this is not the only thing we are doing. The code does that but the data flow is not immediately obvious. At least, it wasn't immediately obvious to me the first time I read the code. We are handing a context that we hold a reference to by virtue of a closure. This makes the data flow relatively opaque.
Let's accept that that it's true that it's counter intuitive for the sake of argument (I'm unconvinced). In that case, the
I still think I'm not understanding exactly what you are saying. I'm still not clear what "Mito" is; the relevant entities that exist are github.com/elastic/mito/lib, |
|
Here's a simplified version of what's done in CEL Input (in the draft PR) and in mito/lib (existing code). There's a TODO comment showing what I'm aiming for: making the context for HTTP requests be the same as the one given to I see the issue here as Does this help? package main
import (
"context"
"fmt"
"net/http"
"os"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
////////////////////////////////////////////////////////////////////////////////
// Mito - simplified version of the code in mito/lib
////////////////////////////////////////////////////////////////////////////////
func HTTPWithContextOpts(ctx context.Context, client *http.Client, options HTTPOptions) cel.EnvOption {
// simplification: remove initialization of client and limiter
return cel.Lib(httpLib{
client: client,
options: options,
ctx: ctx,
})
}
type HTTPOptions any // simplification
type httpLib struct {
client *http.Client
ctx context.Context
options HTTPOptions
}
func (l httpLib) CompileOptions() []cel.EnvOption { // simplifcation: only the get function
return []cel.EnvOption{
cel.Function("get",
cel.Overload(
"get_string",
[]*cel.Type{cel.StringType},
cel.MapType(cel.StringType, cel.DynType),
cel.UnaryBinding(l.doGet), // simplification: removed catch() wrapper
),
),
}
}
func (httpLib) ProgramOptions() []cel.ProgramOption { return nil } // simplification: no options
func (l httpLib) get(url types.String) (*http.Response, error) {
// simplification: no error handling, auth or headers
// TODO: make the context used for this request match the one provided to ContextEval
req, _ := http.NewRequestWithContext(l.ctx, http.MethodGet, string(url), nil)
// addition: assert that there's a valid span in the context to use as the parent
if sc := trace.SpanFromContext(req.Context()).SpanContext(); sc.IsValid() {
fmt.Println("SpanID in request context:", sc.SpanID())
} else {
fmt.Println("ERROR: No valid span in request context!")
os.Exit(1)
}
return l.client.Do(req)
}
func (l httpLib) doGet(arg ref.Val) ref.Val {
// simplification: do the get, but don't handle errors or the actual response
l.get(arg.(types.String))
return types.DefaultTypeAdapter.NativeToValue(make(map[string]interface{}))
}
////////////////////////////////////////////////////////////////////////////////
// CEL Input
////////////////////////////////////////////////////////////////////////////////
func main() {
// setup
otelTracerProvider := sdktrace.NewTracerProvider()
otelTracer := otelTracerProvider.Tracer("github.com/elastic/beats/v7/x-pack/filebeat/input/cel")
ctx := context.Background() // simplification
// build the CEL program once
opts := []cel.EnvOption{HTTPWithContextOpts(ctx, http.DefaultClient, new(HTTPOptions))}
env, _ := cel.NewEnv(opts...)
ast, _ := env.Compile("get('http://www.example.com/')")
prg, _ := env.Program(ast)
// execute CEL program - this will be done repeatedly
runCtx, _ := otelTracer.Start(ctx, "cel.periodic.run")
execCtx, execSpan := otelTracer.Start(runCtx, "cel.program.execution")
fmt.Println("SpanID in ContextEval context:", execSpan.SpanContext().SpanID())
prg.ContextEval(execCtx, map[string]any{})
}My fix would give --- example.go.orig 2026-01-23 17:27:46.358648890 +0100
+++ example.go 2026-01-23 17:32:23.289202073 +0100
@@ -20,19 +20,27 @@
func HTTPWithContextOpts(ctx context.Context, client *http.Client, options HTTPOptions) cel.EnvOption {
// simplification: remove initialization of client and limiter
return cel.Lib(httpLib{
client: client,
options: options,
- ctx: ctx,
+ ctxFn: func() context.Context { return ctx },
+ })
+}
+
+func HTTPWithContextFnOpts(ctxFn func() context.Context, client *http.Client, options HTTPOptions) cel.EnvOption {
+ return cel.Lib(httpLib{
+ client: client,
+ options: options,
+ ctxFn: ctxFn,
})
}
type HTTPOptions any // simplification
type httpLib struct {
client *http.Client
- ctx context.Context
+ ctxFn func() context.Context
options HTTPOptions
}
func (l httpLib) CompileOptions() []cel.EnvOption { // simplifcation: only the get function
return []cel.EnvOption{
@@ -49,12 +57,11 @@
func (httpLib) ProgramOptions() []cel.ProgramOption { return nil } // simplification: no options
func (l httpLib) get(url types.String) (*http.Response, error) {
// simplification: no error handling, auth or headers
- // TODO: make the context used for this request match the one provided to ContextEval
- req, _ := http.NewRequestWithContext(l.ctx, http.MethodGet, string(url), nil)
+ req, _ := http.NewRequestWithContext(l.ctxFn(), http.MethodGet, string(url), nil)
// addition: assert that there's a valid span in the context to use as the parent
if sc := trace.SpanFromContext(req.Context()).SpanContext(); sc.IsValid() {
fmt.Println("SpanID in request context:", sc.SpanID())
} else {
@@ -80,17 +87,19 @@
otelTracer := otelTracerProvider.Tracer("github.com/elastic/beats/v7/x-pack/filebeat/input/cel")
ctx := context.Background() // simplification
// build the CEL program once
- opts := []cel.EnvOption{HTTPWithContextOpts(ctx, http.DefaultClient, new(HTTPOptions))}
+ ctxForEval := ctx
+ opts := []cel.EnvOption{HTTPWithContextFnOpts(func() context.Context { return ctxForEval }, http.DefaultClient, new(HTTPOptions))}
env, _ := cel.NewEnv(opts...)
ast, _ := env.Compile("get('http://www.example.com/')")
prg, _ := env.Program(ast)
// execute CEL program - this will be done repeatedly
runCtx, _ := otelTracer.Start(ctx, "cel.periodic.run")
execCtx, execSpan := otelTracer.Start(runCtx, "cel.program.execution")
fmt.Println("SpanID in ContextEval context:", execSpan.SpanContext().SpanID())
- prg.ContextEval(execCtx, map[string]any{})
+ ctxForEval = execCtx
+ prg.ContextEval(ctxForEval, map[string]any{})
} |
👍
Okay. It's also the context used to handle cancellation of the evaluation.
Thanks. Yeah, I think it's good to compare alternate implementations now. That code and diff in my last comment was just reproducing what's in the original PR, but showing both sides in a shorter example. I think these are the options we're looking at:
I see that option 2 preserves the function signature of |
|
I looked at the diffs you sent @efd6, thanks. They showed what I expected the optional interface approach to be (option 2 from my last comment). Can you say why that is better? |
The reason that I prefer it is that it doesn't proliferate function signatures. |
|
Ok. I prefer the additional function signature because:
I have two ideas:
|
|
If we can do it without altering the lib, I would prefer that. |
|
Closing in favor of an approach that can be done with changes the CEL input only. |


Proposed commit message