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

[am list] Add subcommand to instrument source code #152

Merged
merged 3 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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)
gagbo marked this conversation as resolved.
Show resolved Hide resolved
/// 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,
gagbo marked this conversation as resolved.
Show resolved Hide resolved
/// - 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.");
gagbo marked this conversation as resolved.
Show resolved Hide resolved
}

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))
gagbo marked this conversation as resolved.
Show resolved Hide resolved

(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"))
gagbo marked this conversation as resolved.
Show resolved Hide resolved
Loading