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

Feature Request: Azure Log Analytics Appender #2

Open
samssann opened this issue Nov 12, 2020 · 8 comments
Open

Feature Request: Azure Log Analytics Appender #2

samssann opened this issue Nov 12, 2020 · 8 comments

Comments

@samssann
Copy link

samssann commented Nov 12, 2020

Log appender that sends logs to Azure Log Analytics.
Azure Log analytics API reference https://docs.microsoft.com/en-us/rest/api/loganalytics/create-request

Azure Log Analytics would allow sending application logs to a centralized location, where they can be analysed using Kusto Query Language (KQL) and set to trigger automated alerts based on these queries.

Edit: Added use case.

@s-fleck
Copy link
Owner

s-fleck commented Nov 12, 2020

Thanks definitely possible and probably not hard to do, but it might be a while till I get around to implementing new appenders... A pull request would be appreciated though :)

@samssann
Copy link
Author

samssann commented Nov 12, 2020

Roger! I think I won't have time until maybe the start of next year for a full PR, but I did some preliminary work. AzureAuth can't be used since the workspace_id and workspace_key are not associated with Azure AD, but the Log Analytics workspace itself. The dependency requirements grew a bit, but these can be bypassed with more code.

#' @importFrom rlang is_scalar_character abort
#' @importFrom xml2 as_list
#' @importFrom httr POST add_headers content_type_json content
#' @importFrom glue glue
#' @importFrom tibble is_tibble
#' @importFrom digest hmac
#' @importFrom lubridate tz
#' @importFrom jsonlite toJSON base64_enc base64_dec
azure_write_logs <- function(tbl, log_type, workspace_id, workspace_key) {
  stopifnot(
    is_tibble(tbl), 
    is_scalar_character(log_type),
    is_scalar_character(workspace_id),
    is_scalar_character(workspace_key)
  )
  url <- as.character(glue("https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"))
  content <- as.character(toJSON(tbl, auto_unbox = T))
  content_len <- nchar(content)
  content_size <- as.numeric(gsub(" bytes", "", object.size(content)))
  if(content_size > 30000000L) abort("<api_error> tbl cannot exceed size of 30 MB")
  datetime <- Sys.time()
  tz(datetime) <- "GMT"
  datetime <- format(datetime, "%a, %d %b %Y %X %Z")
  message <- as.character(glue('POST\n{content_len}\napplication/json\nx-ms-date:{datetime}\n/api/logs'))
  decoded_key <- rawToChar(base64_dec(workspace_key))
  encoded_hash <- base64_enc(rawToChar(hmac(raw = T, key = decoded_key, object = message, algo = "sha256")))
  signature <- as.character(glue("SharedKey {workspace_id}:{encoded_hash}"))
  response <- POST(
    url,
    content_type_json(),
    add_headers(
      `Authorization` = signature,
      `Log-Type` = log_type,
      `x-ms-date` = datetime
    ),
    body = content
  )
  if(response$status_code != 200L) abort(glue("<api_error> {paste0(xml2::as_list(content(response))$html$body, collapse = '')}")) # abort if request did not go through
  invisible(TRUE)
}

@s-fleck
Copy link
Owner

s-fleck commented Nov 12, 2020

ok thanks that is helpful. my main goal is right now to get lgrExtra ready for cran till January. I'll look into this issue after that

@samssann
Copy link
Author

Best of luck. Many thanks for this and the lgr package. I'll let you know if I start working this issue beforehand.

@samssann
Copy link
Author

samssann commented Apr 1, 2021

Have you had a chance to look the implementation on this? It seems pretty straight forward if we create a new appender (AppenderAzureLog) than initializes like this

aal <- AppenderAzureLog$new(
  workspace_id = "xxx",
  workspace_key = "xxx",
  log_type = "test"
)

This appender should inherit the AppenderJSON (as json format is used with the http requests body)?

I did a mini proof-of-concept and it worked

# this should be a private method in the AppenderAzureLog class
upload_logs <- function(json, log_type, workspace_id, workspace_key) {
  stopifnot(
    inherits(json, "json"), 
    is_scalar_character(log_type),
    is_scalar_character(workspace_id),
    is_scalar_character(workspace_key)
  )
  url <- as.character(glue("https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"))
  content <- as.character(json)
  content_len <- nchar(content)
  content_size <- as.numeric(gsub(" bytes", "", object.size(content)))
  if(content_size > 30000000L) abort("<api_error> json cannot exceed size of 30 MB") # any ideas on how to present errors so that it matches your general idea of the package?
  datetime <- Sys.time()
  tz(datetime) <- "GMT"
  datetime <- format(datetime, "%a, %d %b %Y %X %Z")
  message <- as.character(glue('POST\n{content_len}\napplication/json\nx-ms-date:{datetime}\n/api/logs'))
  decoded_key <- rawToChar(base64_dec(workspace_key))
  encoded_hash <- base64_enc(rawToChar(hmac(raw = T, key = decoded_key, object = message, algo = "sha256")))
  signature <- as.character(glue("SharedKey {workspace_id}:{encoded_hash}"))
  response <- POST(
    url,
    content_type_json(),
    add_headers(
      `Authorization` = signature,
      `Log-Type` = log_type,
      `x-ms-date` = datetime,
      `time-generated-field` = "timestamp" # points to the timestamp field is json
    ),
    body = content
  )
  if(response$status_code != 200L) abort(glue("<api_error> {paste0(xml2::as_list(content(response))$html$body, collapse = '')}")) # abort if request did not go through
  invisible(TRUE)
}
# create event
event <- LogEvent$new(
  logger = Logger$new("dummy logger"),
  level = 200,
  timestamp = Sys.time(),
  caller = NA_character_,
  msg = "a test message",
  custom_field = "LayoutJson can handle arbitrary fields"
)
lo <- LayoutJson$new() # json layout
lo$set_timestamp_fmt("%Y-%m-%dT%H:%M:%SZ") # the data ingestion supports only the ISO 8601 format
json <- lo$format_event(event)
json
#> {"level":200,"timestamp":"2021-04-01T11:33:42Z","logger":"dummy logger","caller":null,"msg":"a test message","custom_field":"LayoutJson can handle arbitrary fields"}
upload_logs(json, "test", .id, .key) # upload log event

After waiting for a couple of minutes, this appeared in the Azure Log Analytics portal
image
A "_CL" suffix is added automatically to the log_type variable in the ingestion phase to point out that this indeed a custom log. The only "hard" thing is to handle errors in the http request and only delete events from the buffer if the upload is successful. But I think the framework you've done already pretty much enables this. The API has not changed in years so keeping this up-to-date seems easy.

@s-fleck
Copy link
Owner

s-fleck commented Apr 1, 2021

Probably there should be a new AppenderHttp or AppenderRest that uses LayoutJson, as Appenders are responsible for the destination, and Layouts for the format. AppenderJson itself is kinda bad design from that standpoint, but it's really just a convenient shortcut for AppenderFile with LayoutJson.

I'm kinda busy with other things at the moment, so I'm not sure if I get around to it... If you have some experience with R6 classes you could probably hack together your own appender for the time beeing...

Anyways If I might get around to it, I would need to be able to setup a test destination. Is that easy/free with azure?

@samssann
Copy link
Author

samssann commented Apr 1, 2021

Understood. Its pretty easy to setup (and free https://azure.microsoft.com/en-us/services/monitor/). I'll manage to create a custom appender for myself, but I'll probably leave the AppenderHttp/Rest design for you when you have the time. I can share my fork when I get to building it.

@s-fleck
Copy link
Owner

s-fleck commented Apr 1, 2021

Ok cool thanks :) I'll hope I'll have some time to work more on lgr later this year but at the moment I'm pretty swamped at work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants