Skip to content

Commit

Permalink
[am list] Add subcommand to instrument source code (#152)
Browse files Browse the repository at this point in the history
* [am list] Add trait to instrument source code

* Update CHANGELOG

* Various fixes

Parallelize function listing and instrumentation

Add prompt when we can detect potentially hard to revert effects
  • Loading branch information
gagbo authored Nov 2, 2023
1 parent 277891a commit 0e657c9
Show file tree
Hide file tree
Showing 19 changed files with 1,323 additions and 141 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update Rust dependencies (#150)
- Update default versions of Prometheus and Pushgateway (#150)
- Add ability to scrape the metrics of `am`s own web server with `am start --scrape-self` (#153)
- `am list` now properly detects functions instrumented in Typescript using the `Autometrics` decorator (#152)
- `am instrument` is a new subcommand that can automatically add annotations to instrument a project (#152)
+ it works in Go, Python, Typescript, and Rust projects

## [0.5.0]

Expand Down
58 changes: 58 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions am/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ futures-util = { version = "0.3.28", features = ["io"] }
hex = "0.4.3"
http = "0.2.9"
humantime = { workspace = true }
ignore = "0.4.20"
include_dir = "0.7.3"
indicatif = "0.17.5"
itertools = "0.11.0"
Expand Down
10 changes: 10 additions & 0 deletions am/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tracing::info;

mod explore;
mod init;
mod instrument;
mod list;
mod proxy;
pub mod start;
Expand Down Expand Up @@ -64,6 +65,14 @@ pub enum SubCommands {
/// List the functions in a project
List(list::Arguments),

/// Instrument a project entirely.
///
/// IMPORTANT: This will add code in your files! If you want to easily
/// undo the effects of this command, stage your work in progress (using `git add` or similar)
/// So that a command like `git restore .` can undo all unstaged changes, leaving your work
/// in progress alone.
Instrument(instrument::Arguments),

#[clap(hide = true)]
MarkdownHelp,
}
Expand All @@ -86,6 +95,7 @@ pub async fn handle_command(app: Application, config: AmConfig, mp: MultiProgres
}
SubCommands::Update(args) => update::handle_command(args, mp).await,
SubCommands::List(args) => list::handle_command(args),
SubCommands::Instrument(args) => instrument::handle_command(args),
SubCommands::MarkdownHelp => {
let disable_toc = true;
clap_markdown::print_help_markdown::<Application>(Some(disable_toc));
Expand Down
159 changes: 159 additions & 0 deletions am/src/commands/instrument.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use crate::interactive;
use am_list::Language;
use anyhow::Context;
use clap::{Args, Subcommand};
use std::{
path::{Path, PathBuf},
process,
};
use tracing::info;

#[derive(Args)]
pub struct Arguments {
#[command(subcommand)]
command: Command,
}

#[derive(Subcommand)]
enum Command {
/// Instrument functions in a single project, giving the language implementation
///
/// IMPORTANT: This will add code in your files! If you want to easily
/// undo the effects of this command, stage your work in progress (using `git add` or similar)
/// So that a command like `git restore .` can undo all unstaged changes, leaving your work
/// in progress alone.
Single(SingleProject),
/// Instrument functions in all projects under the given directory, detecting languages on a best-effort basis.
///
/// IMPORTANT: This will add code in your files! If you want to easily
/// undo the effects of this command, stage your work in progress (using `git add` or similar)
/// So that a command like `git restore .` can undo all unstaged changes, leaving your work
/// in progress alone.
All(AllProjects),
}

#[derive(Args)]
struct SingleProject {
/// Language to detect autometrics functions for. Valid values are:
/// - 'rust' or 'rs' for Rust,
/// - 'go' for Golang,
/// - 'typescript', 'ts', 'javascript', or 'js' for Typescript/Javascript,
/// - 'python' or 'py' for Python.
#[arg(short, long, value_name = "LANGUAGE", verbatim_doc_comment)]
language: Language,
/// Root of the project to start the search on:
/// - For Rust projects it must be where the Cargo.toml lie,
/// - For Go projects it must be the root of the repository,
/// - For Python projects it must be the root of the library,
/// - For Typescript projects it must be where the package.json lie.
#[arg(value_name = "ROOT", verbatim_doc_comment)]
root: PathBuf,
/// A list of patterns to exclude from instrumentation. The patterns follow .gitignore rules, so
/// `--exclude "/vendor/"` will exclude all the vendor subdirectory only at the root, and adding
/// a pattern that starts with `!` will unignore a file or directory
#[arg(short, long, value_name = "PATTERNS")]
exclude: Vec<String>,
}

#[derive(Args)]
struct AllProjects {
/// Main directory to start the subprojects search on. am currently detects
/// Rust (Cargo.toml), Typescript (package.json), and Golang (go.mod)
/// projects.
#[arg(value_name = "ROOT")]
root: PathBuf,
/// A list of patterns to exclude from instrumentation. The patterns follow .gitignore rules, so
/// `--exclude "/vendor/"` will exclude all the vendor subdirectory only at the root, and adding
/// a pattern that starts with `!` will unignore a file or directory
#[arg(short, long, value_name = "PATTERNS")]
exclude: Vec<String>,
}

pub fn handle_command(args: Arguments) -> anyhow::Result<()> {
match args.command {
Command::Single(args) => handle_single_project(args),
Command::All(args) => handle_all_projects(args),
}
}

fn folder_has_unstaged_changes(root: &Path) -> Option<bool> {
if cfg!(windows) {
// TODO: figure out the Windows story
return None;
}

if cfg!(unix) {
// TODO: Figure out the non git story
let git_diff = process::Command::new("git")
.arg("-C")
.arg(root.as_os_str())
.arg("diff")
.output();
return match git_diff {
Ok(output) => Some(!output.stdout.is_empty()),
Err(_) => {
// We either don't have git, or root is not within a repository
None
}
};
}

None
}

fn handle_all_projects(args: AllProjects) -> Result<(), anyhow::Error> {
let root = args
.root
.canonicalize()
.context("The path must be resolvable to an absolute path")?;

if let Some(true) = folder_has_unstaged_changes(&root) {
let cont = interactive::confirm("The targeted root folder seems to have unstaged changes. `am` will also change files in this folder.\nDo you wish to continue?")?;
if !cont {
return Ok(());
}
}

info!("Instrumenting functions in {}:", root.display());

let mut exclude_patterns_builder = ignore::gitignore::GitignoreBuilder::new(&root);
for pattern in args.exclude {
exclude_patterns_builder.add_line(None, &pattern)?;
}
let exclude_patterns = exclude_patterns_builder.build()?;

am_list::instrument_all_project_files(&root, &exclude_patterns)?;

println!("If your project has Golang files, you need to run `go generate` now.");

Ok(())
}

fn handle_single_project(args: SingleProject) -> Result<(), anyhow::Error> {
let root = args
.root
.canonicalize()
.context("The path must be resolvable to an absolute path")?;

if let Some(true) = folder_has_unstaged_changes(&root) {
let cont = interactive::confirm("The targeted root folder seems to have unstaged changes. `am` will also change files in this folder.\nDo you wish to continue?")?;
if !cont {
return Ok(());
}
}
info!("Instrumenting functions in {}:", root.display());

let mut exclude_patterns_builder = ignore::gitignore::GitignoreBuilder::new(&root);
for pattern in args.exclude {
exclude_patterns_builder.add_line(None, &pattern)?;
}
let exclude_patterns = exclude_patterns_builder.build()?;

am_list::instrument_single_project_files(&root, args.language, &exclude_patterns)?;

if args.language == Language::Go {
println!("You need to run `go generate` now.");
}

Ok(())
}
4 changes: 0 additions & 4 deletions am/src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ use clap::{Args, Subcommand};
use std::path::PathBuf;
use tracing::info;

// TODO(gagbo): add an additional subcommand that makes use of am_list::find_roots to
// list all the functions under a given folder, by detecting the languages and all the
// subprojects included.

#[derive(Args)]
pub struct Arguments {
#[command(subcommand)]
Expand Down
2 changes: 2 additions & 0 deletions am_list/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ license.workspace = true

[dependencies]
anyhow = "1.0.71"
crop = "0.4.0"
ignore = "0.4.20"
itertools = "0.11.0"
log = "0.4.18"
rayon = "1.7.0"
Expand Down
7 changes: 6 additions & 1 deletion am_list/runtime/queries/typescript/all_functions.scm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
(lexical_declaration
(variable_declarator
name: (identifier) @func.name
value: (arrow_function)))
value: (arrow_function) @func.value))

(lexical_declaration
(variable_declarator
name: (identifier) @func.name
value: (function) @func.value))

(class_declaration
name: (type_identifier) @type.name
Expand Down
11 changes: 11 additions & 0 deletions am_list/runtime/queries/typescript/autometrics.scm
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@
(method_definition
name: (property_identifier) @method.name)]))
(#eq? @decorator.name "Autometrics"))

((class_declaration
decorator: (decorator (call_expression
function: (identifier) @decorator.name))
name: (type_identifier) @type.name
body: (class_body
[(method_signature
name: (property_identifier) @method.name)
(method_definition
name: (property_identifier) @method.name)]))
(#eq? @decorator.name "Autometrics"))
Loading

0 comments on commit 0e657c9

Please sign in to comment.