portal

sheluchin 2024-03-02T21:34:08.159209Z

When I see an exception reported, the type of the object says it's a PersistentArrayMap:

; eval (root-form): (type @user/p)
clojure.lang.PersistentArrayMap
Is there some way to get the actual exception here?

sheluchin 2024-03-04T15:18:19.221919Z

Hmm, I kinda have it hooked up by just dumping all of that code into my project's user.clj, but it looks like unless I explicitly tap> something, it doesn't go to the "important" Portal window, but it does go to ** logging **. Is this the intended behaviour of your config? Edit: re-read your blog post and saw: > • one named *logging* that gets the output from the middleware (from regular evaluation of expressions) and the output from clojure.tools.logging (if present) Which answers my question from above, if I understand correctly. Just to confirm, the only things that should be sent to the "important" Portal window are those which come from direct tap> calls in your code? I also notice that the log message isn't showing up in the Portal viewer. Is this a bug in my setup code?

sheluchin 2024-03-04T15:28:44.711219Z

djblue 2024-03-04T16:24:19.314239Z

I haven't gone through the entire thread, but you can get access to the original exception from the datafied exception via (-> @user/p meta :obj)

👍🏻 1
1
seancorfield 2024-03-04T18:18:40.597319Z

> Just to confirm, the only things that should be sent to the "important" Portal window are those which come from direct tap> calls in your code? Correct. Logging and regular (middleware) evaluation appear in the ** logging ** window and explicit tap>'s appear in the directory-labeled window. > I also notice that the log message isn't showing up in the Portal viewer. Is this a bug in my setup code? Hard to say. It looks like that c.t.l. debug call is in your user.clj file? Without seeing all your setup code, kinda hard to guess whether it is right...

sheluchin 2024-03-05T17:59:13.788719Z

@seancorfield yes, I basically just lifted that portion of your code directly into my user.clj. What's throwing me off is I except the log message (stdio) to appear as the "main text" (not sure if there's a technical term for this) of the viewer, rather than showing nil as it is in my screenshot there. Do you have that same behaviour, or do you see similar to "my error log" as in the top image here: https://cljdoc.org/d/djblue/portal/0.52.2/doc/guides/logging/timbre ?

seancorfield 2024-03-05T18:45:35.015859Z

Oh, I have no idea about Timbre. My code only deals with clojure.tools.logging. We use log4j2 under the hood.

seancorfield 2024-03-05T18:45:58.310289Z

And logging calls eval to nil so I would expect to see nil as the result.

sheluchin 2024-03-05T19:26:37.159939Z

Ah, yeah, that makes sense as the result. Do you not find it a little awkward to have to manually expand logging messages there to understand what's going on?

seancorfield 2024-03-05T19:56:38.219929Z

Not sure what you mean... The log messages show up in Portal much as they'd show up in the console or log file...

seancorfield 2024-03-05T20:02:37.989149Z

Here's a screenshot of my setup -- tap>'d values on top, logging/middleware output below:

seancorfield 2024-03-05T20:03:13.105679Z

The green log entries are INFO and they show the message and filename and line no.

sheluchin 2024-03-05T21:01:57.933039Z

sheluchin 2024-03-05T21:02:48.179719Z

That looks really good. Not what I'm seeing. I attached my user.clj above, in case anything jumps out at you. There's an RCF at the bottom that produces what you see in this screenshot.

seancorfield 2024-03-05T21:22:30.129299Z

And when that user.clj code runs, you see

Logging will be tap>'d...
Starting  as the REPL...

seancorfield 2024-03-05T21:23:48.015169Z

And you have two Portal windows open by that point too?

seancorfield 2024-03-05T21:24:06.578639Z

(and then you connect to that running REPL -- not jack-in -- right?)

sheluchin 2024-03-05T21:26:36.423629Z

Yep, it's in the other terminal window where I start everything up. Note that I do use Timbre, but I'm testing c.t.l and presuming there are no bad interactions with the rest of my stuff:

$ npx shadow-cljs server -A:dev:repl/conjure:shadow-cljs:test:dev.repl/logging:cards
shadow-cljs - config: /home/alex/repos/seqfind/shadow-cljs.edn
shadow-cljs - starting via "clojure"
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See  for further details.
SLF4J: Class path contains SLF4J bindings targeting slf4j-api versions prior to 1.8.
SLF4J: Ignoring binding found at [jar:file:/home/alex/.m2/repository/com/fzakaria/slf4j-timbre/0.3.21/slf4j-timbre-0.3.21.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See  for an explanation.
Logging will be tap>'d...
Starting nREPL Server with Portal, Notebook, CIDER as the REPL...
nREPL server started on port 36255 on host localhost - 
> And you have two Portal windows open by that point too? Yes, both Portal windows open after I run the above. They seem to work exactly as you would expect, but not the logging portion. > (and then you connect to that running REPL -- not jack-in -- right?) That's right. I connect to ; localhost:36255 (connected): .nrepl-port.

seancorfield 2024-03-02T23:11:41.998579Z

I don't recognize that output -- how are you using Portal? I get actual exceptions reported in my setup?

sheluchin 2024-03-02T23:29:06.792209Z

@seancorfield the attached screenshot shows what I see. I expect that when I deref the Portal atom (`user/p`) and check out the type, it should be a PSQLException, but it shows me it's that PersistentArrayMap.

(type (Exception.)))  ; java.lang.Exception
That ☝️ makes sense, and when I select it in Portal and deref the atom, I get java.lang.Exception as expected.

seancorfield 2024-03-02T23:38:28.657959Z

Oh, because the exception has been run through Throwable->map by that point, so you'd need to navigate into the map to get at the :type keys

seancorfield 2024-03-02T23:40:02.076899Z

You could probably eval *e to get the original exception (depending on how your nREPL setup is configured). I have that bound to a hot key so I can easily see the raw exception.

👍 1
seancorfield 2024-03-02T23:40:34.221189Z

Given that Portal is showing you the full details, why do you want to eval @user/p?

sheluchin 2024-03-02T23:42:14.210689Z

I want to try calling different methods of the exception instance.

seancorfield 2024-03-02T23:42:15.007959Z

Basically, Portal has datafy'd it so it can present the view you're seeing (and datafy on an exception calls Throwable->map).

seancorfield 2024-03-02T23:42:41.049489Z

Then *e is probably what you want...?

sheluchin 2024-03-02T23:43:56.892119Z

Yeah, that'll do it. I forgot that exists. Thank you for the reminder on that point. But out of curiosity, is it possible to reverse the datafying?

seancorfield 2024-03-02T23:45:25.014079Z

Not in general, no. Sometimes it's a lossy representation. Sometimes it can be reversed but it's specific to the transformation used to produce the data.

seancorfield 2024-03-02T23:46:37.948829Z

In this particular case, you probably couldn't reconstruct the original object because the PG-specific exception information is not present in the datafied result, only the general Exception stuff.

seancorfield 2024-03-02T23:47:09.663459Z

(and quite often the constructors you might need to rebuild the Java objects aren't public)

sheluchin 2024-03-02T23:48:09.738489Z

Isn't the exception information all in the second item of the :via vector in that screenshot?

seancorfield 2024-03-02T23:53:09.815129Z

Only the generic stuff. None of the PG-specific stuff.

seancorfield 2024-03-02T23:57:15.932939Z

I don't think it even includes the "generic" SQL-specific exception stuff. Just message/type/cause and stacktrace. Plus data if it's an ex-info.

sheluchin 2024-03-02T23:58:08.453569Z

Ah, I get what you mean. Like .getSQLState wouldn't work because that's pg-specific. That's what your talking about?

seancorfield 2024-03-02T23:59:14.666169Z

Well, .getSQLState() is "generic" SQL-specific I think but, yeah, that's not included.

seancorfield 2024-03-02T23:59:43.177569Z

You might get useful data from (bean *e) or use clojure.java.data's from-java on it to get more detail.

seancorfield 2024-03-03T00:00:58.421899Z

For example:

user=> (bean (ex-info "X" {}))
{:cause nil, :class clojure.lang.ExceptionInfo, :data {}, :localizedMessage "X", :message "X", :stackTrace #object["[Ljava.lang.StackTraceElement;" 0x3c818ac4 "[Ljava.lang.StackTraceElement;@3c818ac4"], :suppressed #object["[Ljava.lang.Throwable;" 0x5b69d40d "[Ljava.lang.Throwable;@5b69d40d"]}
user=> (map bean (:stackTrace *1))
({:classLoaderName nil, :fileName "NO_SOURCE_FILE", :moduleVersion nil, :nativeMethod false, :className "user$eval13", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 1, :methodName "invokeStatic"} {:classLoaderName nil, :fileName "NO_SOURCE_FILE", :moduleVersion nil, :nativeMethod false, :className "user$eval13", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 1, :methodName "invoke"} {:classLoaderName "app", :fileName "Compiler.java", :moduleVersion nil, :nativeMethod false, :className "clojure.lang.Compiler", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 7570, :methodName "eval"} {:classLoaderName "app", :fileName "Compiler.java", :moduleVersion nil, :nativeMethod false, :className "clojure.lang.Compiler", :moduleName nil, :cl...

seancorfield 2024-03-03T00:01:37.317099Z

(you could use *e in that first call)

seancorfield 2024-03-03T00:03:03.190939Z

user=> (throw (ex-info "X" {}))
Execution error (ExceptionInfo) at user/eval17 (REPL:1).
X
user=> (bean *e)
{:cause nil, :class clojure.lang.ExceptionInfo, :data {}, :localizedMessage "X", :message "X", :stackTrace #object["[Ljava.lang.StackTraceElement;" 0xec50f54 "[Ljava.lang.StackTraceElement;@ec50f54"], :suppressed #object["[Ljava.lang.Throwable;" 0x5b69d40d "[Ljava.lang.Throwable;@5b69d40d"]}
user=> (map bean (:stackTrace *1))
({:classLoaderName nil, :fileName "NO_SOURCE_FILE", :moduleVersion nil, :nativeMethod false, :className "user$eval17", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 1, :methodName "invokeStatic"} {:classLoaderName nil, :fileName "NO_SOURCE_FILE", :moduleVersion nil, :nativeMethod false, :className "user$eval17", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 1, :methodName "invoke"} {:classLoaderName "app", :fileName "Compiler.java", :moduleVersion nil, :nativeMethod false, :className "clojure.lang.Compiler", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 7570, :methodName "eval"} {:classLoaderName "app", :fileName "Compiler.java", :moduleVersion nil, :nativeMethod false, :className "clojure.lang.Compiler", :moduleName nil, :class java.lang.StackTraceElement, :lineNumber 7525, :methodName "eval"} {:classLoaderName "app", :fileName "core.clj", :moduleVersion ...

sheluchin 2024-03-03T17:29:53.522999Z

Good stuff, @seancorfield. Thank you for adding the bean usage examples. Last night I also read your blog post about the workflow you use. I'm going to borrow some of your hotkey ideas from dot-clojure and I'd really adapt the two-Portals solution you use for "important" + logging Portal windows. I think it's a little more involved for me to get that part going since I use nvim and not Calva, but your examples should suffice in figuring it all out when I get to it down the line.

seancorfield 2024-03-03T17:50:14.491089Z

Sure. If you change the two open calls in this code to not specify VS Code as the launcher, you will get two Portal browser windows: https://github.com/seancorfield/vscode-calva-setup/blob/develop/calva/config.edn#L46-L90 And this is the code that wires up clojure.tools.logging to Portal when starting a REPL (and also handles the middleware stuff, below): https://github.com/seancorfield/dot-clojure/blob/develop/src/org/corfield/dev/repl.clj#L87-L120