Skip to content
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

Fix: Improves Support + Bugfix #7

Merged
merged 13 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@ The LogAnalyzer open-source app is a simple, plug and play application developed

# How it works?

- The LogAnalyzer app uses the Posit Connect API to fetch logs for application run jobs, semantically colouring them so they are easier to read.
- The only thing you need to set this up and use is the `CONNECT_API_KEY` set as an environment variable. The idea is that the key should come from either an admin account or someone with privileges to view all apps. Apps that are not available to a user will not have their logs available to them.
- If you want to test the app locally, you will need to set the `CONNECT_SERVER` as an environment variable. When deployed, the `CONNECT_SERVER` is setup automatically for you.
- The LogAnalyzer app uses the Posit Connect API to fetch logs for application run jobs, semantically colouring them so they are easier to read.
- The only thing you need to set this up and use is the `CONNECT_API_KEY` set as an environment variable. The idea is that the key should come from either an admin account or someone with privileges to view all apps. Apps that are not available to a user will not have their logs available to them.
- If you want to test the app locally, you will need to set the `CONNECT_SERVER` as an environment variable. When deployed, the `CONNECT_SERVER` is setup automatically for you.

![](img/app_preview.gif)

# Configuration

- `[app/logic/api_utils.R` - `get_app_list()]` Posit Connect differentiates apps with two `app_role` values: `owner` and `viewer`. You can toggle between these using the `config.yml` file. The set value is `owner`. If you want to use both together, you can simply set the value to a blank character `""`.

# Credits
It was our collaboration with <img src="img/elkem_logo.png" alt="Elkem" width="50"/> which led to the creation of this app. The initial idea came from use-cases where we realised we wanted to track all the logs and be able to read them properly since Posit Connect was the de facto deployment environment. When we made this app, we realised there was potential in sharing this with the rest of the community and invite everyone to use it and add it. We appreciate and thank Elkem for their openness to share it with the world.

It was our collaboration with <img src="img/elkem_logo.png" alt="Elkem" width="50"/> which led to the creation of this app. The initial idea came from use-cases where we realised we wanted to track all the logs and be able to read them properly since Posit Connect was the de facto deployment environment. When we made this app, we realised there was potential in sharing this with the rest of the community and invite everyone to use it and add it. We appreciate and thank Elkem for their openness to share it with the world.

You can read more about Appsilon and Elkem's collaboration on our case study [here](https://www.appsilon.com/case-studies/refining-elkems-processes-with-advanced-data-analytics).

## Appsilon

<img src="https://avatars0.githubusercontent.com/u/6096772" align="right" alt="" width="6%" />
<img src="https://avatars0.githubusercontent.com/u/6096772" align="right" width="6%"/>

Appsilon is a **Posit (formerly RStudio) Full Service Certified Partner**.<br/>
Learn more at [appsilon.com](https://appsilon.com).
Appsilon is a **Posit (formerly RStudio) Full Service Certified Partner**.<br/> Learn more at [appsilon.com](https://appsilon.com).

Get in touch [[email protected]](mailto:[email protected])
Get in touch [opensource\@appsilon.com](mailto:[email protected])

Explore the [Rhinoverse](https://rhinoverse.dev) - a family of R packages built around [Rhino](https://appsilon.github.io/rhino/)!

<a href = "https://appsilon.us16.list-manage.com/subscribe?u=c042d7c0dbf57c5c6f8b54598&id=870d5bfc05" target="_blank">
<img id="footer-banner" src="https://raw.githubusercontent.com/Appsilon/website-cdn/gh-pages/shiny_weekly_light.jpg" alt="Subscribe for Shiny tutorials, exclusive articles, R/Shiny community events, and more."/>
</a>
<a href = "https://appsilon.us16.list-manage.com/subscribe?u=c042d7c0dbf57c5c6f8b54598&id=870d5bfc05" target="_blank"> <img src="https://raw.githubusercontent.com/Appsilon/website-cdn/gh-pages/shiny_weekly_light.jpg" alt="Subscribe for Shiny tutorials, exclusive articles, R/Shiny community events, and more." id="footer-banner"/> </a>
24 changes: 19 additions & 5 deletions app/logic/api_utils.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
box::use(
config[get],
dplyr[filter],
glue[glue],
httr2[
Expand All @@ -11,6 +12,7 @@ box::use(
],
jsonlite[fromJSON],
magrittr[`%>%`],
shiny[isTruthy],
)

#' Simple function to get the access token from environment
Expand Down Expand Up @@ -42,15 +44,19 @@ get_api_url <- function(

#' Function to get a list of all apps belonging to the token
#'
#' @param app_mode_filter Character. The filter for app_mode in the API
#' response. Default is "shiny".
#' @param app_mode_filter Character list. The filter for app_mode in the API
#' response. Default is list("shiny", "python-shiny", "quarto-shiny").
#' @param endpoint Character. Default is "content"
#' @param app_role Character. Read from the config.yml file. The possible value
DeepanshKhurana marked this conversation as resolved.
Show resolved Hide resolved
#' for this can be "owner" or "viewer". You can leave it blank in the config as
#' "" to include both.
#' @param dry_run Logical. Whether to dry run the API for debugging.
#' Default is FALSE
#' @export
get_app_list <- function(
app_mode_filter = "shiny",
app_mode_filter = list("shiny", "python-shiny", "quarto-shiny"),
endpoint = "content",
app_role = get("app_role"),
dry_run = FALSE
) {

Expand All @@ -66,11 +72,19 @@ get_app_list <- function(
api_request %>%
req_dry_run()
} else {
api_request %>%
response <- api_request %>%
req_perform() %>%
resp_body_string() %>%
fromJSON() %>%
filter(app_mode == app_mode_filter)
filter(
app_mode %in% app_mode_filter
)

if (isTruthy(app_role)) {
response <- response[response$app_role == app_role, ]
} else {
response
}
}
}

Expand Down
42 changes: 40 additions & 2 deletions app/logic/job_list_utils.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
box::use(
dplyr[
mutate,
select
],
magrittr[
`%>%`
],
shiny[
div,
strong
Expand All @@ -9,12 +16,43 @@ box::use(
app/logic/general_utils[format_timestamp],
)

#' Process the dataframe for job list
#' @param job_list_data the job list data to process for the mod_job_list
#' @export
#'
process_job_data <- function(job_list_data) {
if (length(job_list_data)) {
job_list_data %>%
select(id, key, start_time, end_time) %>%
mutate(
job = paste(
id,
key,
start_time,
end_time,
sep = "_-_"
)
) %>%
select(
-c(
id,
key,
start_time,
end_time
)
)
} else {
data.frame(
job = character()
)
}
}

#' Function to process each row for the job table
#' This creates the HTML for the row
#'
#' @param job_data the job_data for a single job
#' @export
process_job_data <- function(
render_job_data <- function(
job_data
) {
job_info <- strsplit(job_data, "_-_")[[1]]
Expand Down
45 changes: 20 additions & 25 deletions app/view/mod_job_list.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ box::use(
colDef,
getReactableState,
reactable,
reactableLang,
reactableOutput,
renderReactable
],
shiny[
isTruthy,
moduleServer,
NS,
reactive,
Expand All @@ -22,7 +24,10 @@ box::use(

box::use(
app/logic/api_utils[get_job_list],
app/logic/job_list_utils[process_job_data],
app/logic/job_list_utils[
process_job_data,
render_job_data
],
)

#' @export
Expand All @@ -48,25 +53,9 @@ server <- function(id, state) {

output$job_list_table <- renderReactable({

processed_jobs <- job_list_data() %>%
select(id, key, start_time, end_time) %>%
mutate(
job = paste(
id,
key,
start_time,
end_time,
sep = "_-_"
)
) %>%
select(
-c(
id,
key,
start_time,
end_time
)
)
DeepanshKhurana marked this conversation as resolved.
Show resolved Hide resolved
processed_jobs <- process_job_data(
job_list_data()
)

reactable(
data = processed_jobs,
Expand All @@ -76,19 +65,25 @@ server <- function(id, state) {
columns = list(
job = colDef(
cell = function(job_data) {
process_job_data(job_data)
render_job_data(job_data)
}
)
),
language = reactableLang(
noData = "No jobs found."
)
)

})

state$selected_job <- reactive({
index <- getReactableState("job_list_table", "selected")
list(
"key" = job_list_data()[index, ]$key,
"id" = job_list_data()[index, ]$id
)
if (isTruthy(index) && length(job_list_data()) > 0) {
list(
"key" = job_list_data()[index, ]$key,
"id" = job_list_data()[index, ]$id
)
}
})

})
Expand Down
2 changes: 2 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
default:
rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO")
rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA)
app_role: "owner"

23 changes: 0 additions & 23 deletions strings.yml

This file was deleted.

61 changes: 61 additions & 0 deletions tests/testthat/test-job_list_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
box::use(
testthat[
describe,
expect_equal,
it
],
)

box::use(
app/logic/job_list_utils[
process_job_data
],
)

describe("process_job_data()", {

it("returns processed jobs when job_list_data is not empty", {
job_list <- data.frame(
id = c(1, 2),
key = c("key1", "key2"),
start_time = c("2021-01-01 00:00:00", "2021-01-02 00:00:00"),
end_time = c("2021-01-01 12:00:00", "2021-01-02 12:00:00")
)

expected_result <- data.frame(
job = c("1_-_key1_-_2021-01-01 00:00:00_-_2021-01-01 12:00:00",
"2_-_key2_-_2021-01-02 00:00:00_-_2021-01-02 12:00:00")
)

result <- process_job_data(job_list)
expect_equal(result, expected_result)
})

it("returns an empty dataframe when job_list_data is empty", {
job_list <- data.frame(
id = integer(),
key = character(),
start_time = character(),
end_time = character()
)

expected_result <- data.frame(
job = character()
)

result <- process_job_data(job_list)
expect_equal(result, expected_result)
})

it("handles NULL input gracefully", {
job_list <- NULL

expected_result <- data.frame(
job = character()
)

result <- process_job_data(job_list)
expect_equal(result, expected_result)
})

})
Loading