tool_insights_client/src/message.rs (173 lines of code) (raw):
// Copyright 2022 Twitter, Inc.
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use std::env;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::client::Context;
use crate::util::{
decode_zipkin_compatible_id, duration_in_seconds, encode_zipkin_compatible_id, get_cwd,
get_zipkin_compatible_id, merge_maps, seconds_since_time,
};
const SCHEMA_VERSION: u32 = 2;
const TOOL_INSIGHTS_NESTING_LEVEL_ENV_VAR: &str = "TOOL_INSIGHTS_NESTING_LEVEL";
const TOOL_INSIGHTS_SESSION_ID_ENV_VAR: &str = "TOOL_INSIGHTS_SESSION_ID";
const GITSTATS_TRACE_ID: &str = "X_B3_TRACEID";
const GITSTATS_SPAN_ID: &str = "X_B3_SPANID";
const LIBRARY_VERSION: &str = "Rust v1"; // update when changes are made to the library
// Even though there is a provision to have more than one message here, I'm only
// seeing a single message for every record so that's how we will use it.
#[derive(Serialize, Debug)]
pub struct Message {
schema_version: u32,
messages: Vec<MessageBody>,
}
impl Message {
pub fn new(
message_type: MessageKind,
ti_context: &Context,
end_time: Option<SystemTime>,
map: Option<&HashMap<String, String>>,
) -> Message {
let message = MessageBody::new(message_type, ti_context, end_time, map);
let messages = vec![message];
Message {
schema_version: SCHEMA_VERSION,
messages,
}
}
#[allow(dead_code)]
fn add_message(&mut self, message: MessageBody) -> &Message {
self.messages.push(message);
self
}
}
#[derive(Serialize, Debug)]
pub struct MessageBody {
message_type: String,
core_data: CoreData,
#[serde(skip_serializing_if = "Option::is_none")]
duration_seconds: Option<f64>,
}
impl MessageBody {
pub fn new(
message_type: MessageKind,
ti_context: &Context,
end_time: Option<SystemTime>,
map: Option<&HashMap<String, String>>,
) -> MessageBody {
MessageBody {
message_type: message_type.to_string(),
core_data: CoreData::new(ti_context, map),
duration_seconds: end_time.map(|t| duration_in_seconds(ti_context.get_start_time(), t)),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CoreData {
tool_name: String,
tool_version: String,
tool_feature_name: String,
run_id: String,
run_time_epoch: u64,
run_nesting_level: u32,
run_argv: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
run_exit_code: Option<i32>,
run_current_working_directory: String,
session_id: String,
user_username: String,
machine_hostname: String,
#[serde(skip_serializing_if = "Option::is_none")]
custom_map: Option<HashMap<String, String>>,
lib_ver: String,
}
impl CoreData {
fn new(ti_context: &Context, map: Option<&HashMap<String, String>>) -> CoreData {
let final_map: Option<HashMap<String, String>> =
merge_maps(map.cloned(), ti_context.get_custom_map().cloned());
let core_data = CoreData {
tool_name: ti_context.get_tool_name().to_string(),
tool_version: ti_context.get_tool_version().to_string(),
tool_feature_name: ti_context
.get_tool_feature_name()
.unwrap_or("__invocation__")
.to_string(),
run_id: Uuid::new_v4().to_string(),
run_time_epoch: seconds_since_time(ti_context.get_start_time()),
run_nesting_level: get_nesting_level(),
run_argv: env::args().collect(),
run_exit_code: ti_context.get_exit_code(),
run_current_working_directory: get_cwd()
.into_os_string()
.into_string()
.unwrap_or_else(|_| "no_cwd".to_string()),
session_id: get_session_id(),
user_username: whoami::username(),
machine_hostname: whoami::hostname(),
custom_map: final_map,
lib_ver: LIBRARY_VERSION.to_string(),
};
set_env_vars(&core_data);
core_data
}
}
pub enum MessageKind {
ErrorMessage,
PerformanceMessage,
UsageMessage,
}
impl ToString for MessageKind {
fn to_string(&self) -> String {
match *self {
MessageKind::ErrorMessage => {
"com.twitter.toolinsights.messages.ErrorMessage".to_string()
}
MessageKind::PerformanceMessage => {
"com.twitter.toolinsights.messages.PerformanceMessage".to_string()
}
MessageKind::UsageMessage => {
"com.twitter.toolinsights.messages.UsageMessage".to_string()
}
}
}
}
pub fn get_session_id() -> String {
// return value from environment variable, if set, otherwise a new uuid
env::var(TOOL_INSIGHTS_SESSION_ID_ENV_VAR).unwrap_or_else(|_| Uuid::new_v4().to_string())
}
pub fn get_trace_id() -> u64 {
// If the trace id env var is set, use that value, otherwise generate new
match env::var(GITSTATS_TRACE_ID) {
Ok(id_str) => decode_zipkin_compatible_id(id_str),
Err(_) => get_zipkin_compatible_id(),
}
}
pub fn get_span_id(trace_id: u64) -> u64 {
// If the span id env var is set, then generate a new span id, otherwise use trace id
match env::var(GITSTATS_SPAN_ID) {
Ok(_) => {
// span id is set, create a new span id for this process
get_zipkin_compatible_id()
}
Err(_) => {
//span id is not set, use trace id
trace_id
}
}
}
pub fn set_trace_env_vars(trace_id: u64, span_id: u64) {
env::set_var(
GITSTATS_TRACE_ID,
encode_zipkin_compatible_id(trace_id, false),
);
env::set_var(
GITSTATS_SPAN_ID,
encode_zipkin_compatible_id(span_id, false),
);
}
pub fn get_nesting_level() -> u32 {
// return value from environment variable (if set), otherwise `0`
env::var(TOOL_INSIGHTS_NESTING_LEVEL_ENV_VAR)
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0)
}
pub fn set_env_vars(data: &CoreData) {
env::set_var(TOOL_INSIGHTS_SESSION_ID_ENV_VAR, &data.session_id);
env::set_var(
TOOL_INSIGHTS_NESTING_LEVEL_ENV_VAR,
(data.run_nesting_level + 1).to_string(),
);
}