diff --git a/.editorconfig b/.editorconfig index c165dbb..1fea6cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,466 +1,30 @@ +root = true + [*] +insert_final_newline = true charset = utf-8 -end_of_line = lf -indent_size = 2 +trim_trailing_whitespace = true indent_style = space -insert_final_newline = true -max_line_length = 100 -tab_width = 2 -ij_continuation_indent_size = 4 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = true -ij_smart_tabs = false -ij_visual_guides = none -ij_wrap_on_typing = false - -[*.java] -tab_width = 4 -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = false -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = false -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = false -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_align_types_in_multi_catch = false -ij_java_annotation_parameter_wrap = normal -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = normal -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = normal -ij_java_binary_operation_sign_on_next_line = true -ij_java_binary_operation_wrap = normal -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 2 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 0 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_add_space = false -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = normal -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = split_into_lines -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 999 -ij_java_class_names_in_javadoc = 1 -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_not_wrap_after_single_annotation_in_parameter = false -ij_java_do_while_brace_force = always -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = false -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = true -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = true -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = false -ij_java_entity_dd_suffix = EJB -ij_java_entity_eb_suffix = Bean -ij_java_entity_hi_suffix = Home -ij_java_entity_lhi_prefix = Local -ij_java_entity_lhi_suffix = Home -ij_java_entity_li_prefix = Local -ij_java_entity_pk_class = java.lang.String -ij_java_entity_vo_suffix = VO -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = normal -ij_java_extends_list_wrap = normal -ij_java_field_annotation_wrap = split_into_lines -ij_java_finally_on_new_line = false -ij_java_for_brace_force = always -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = normal -ij_java_generate_final_locals = true -ij_java_generate_final_parameters = true -ij_java_if_brace_force = always -ij_java_imports_layout = $*,|,* -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = true -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 3 -ij_java_keep_blank_lines_between_package_declaration_and_header = 3 -ij_java_keep_blank_lines_in_code = 3 -ij_java_keep_blank_lines_in_declarations = 3 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = false -ij_java_keep_first_column_comment = false -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_add_space_on_reformat = false -ij_java_line_comment_at_first_column = true -ij_java_message_dd_suffix = EJB -ij_java_message_eb_suffix = Bean -ij_java_method_annotation_wrap = split_into_lines -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = normal -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = normal -ij_java_modifier_list_wrap = false -ij_java_multi_catch_types_wrap = normal -ij_java_names_count_to_use_import_on_demand = 999 -ij_java_new_line_after_lparen_in_annotation = false -ij_java_new_line_after_lparen_in_record_header = false -ij_java_parameter_annotation_wrap = normal -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = on_every_item -ij_java_rparen_on_new_line_in_annotation = false -ij_java_rparen_on_new_line_in_record_header = false -ij_java_session_dd_suffix = EJB -ij_java_session_eb_suffix = Bean -ij_java_session_hi_suffix = Home -ij_java_session_lhi_prefix = Local -ij_java_session_lhi_suffix = Home -ij_java_session_li_prefix = Local -ij_java_session_si_suffix = Service -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = true -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_annotation_eq = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = true -ij_java_ternary_operation_wrap = on_every_item -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = normal -ij_java_throws_list_wrap = normal -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = split_into_lines -ij_java_visibility = public -ij_java_while_brace_force = always -ij_java_while_on_new_line = false -ij_java_wrap_comments = true -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false +indent_size = 2 +end_of_line = lf -[{*.kt,*.kts}] -indent_style = space -insert_final_newline = true -max_line_length = 120 +[{Makefile,*.go,.gitmodules}] +indent_style = tab indent_size = 4 -ij_continuation_indent_size = 4 -ij_java_names_count_to_use_import_on_demand = 9999 -ij_kotlin_align_in_columns_case_branch = false -ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = false -ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = true -ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true -ij_kotlin_assignment_wrap = normal -ij_kotlin_blank_lines_after_class_header = 0 -ij_kotlin_blank_lines_around_block_when_branches = 0 -ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = true -ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = on_every_item -ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL -ij_kotlin_continuation_indent_for_chained_calls = true -ij_kotlin_continuation_indent_for_expression_bodies = true -ij_kotlin_continuation_indent_in_argument_lists = true -ij_kotlin_continuation_indent_in_elvis = false -ij_kotlin_continuation_indent_in_if_conditions = false -ij_kotlin_continuation_indent_in_parameter_lists = false -ij_kotlin_continuation_indent_in_supertype_lists = false -ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = off -ij_kotlin_finally_on_new_line = false -ij_kotlin_if_rparen_on_new_line = false -ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = * -ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 2 -ij_kotlin_keep_blank_lines_in_code = 2 -ij_kotlin_keep_blank_lines_in_declarations = 2 -ij_kotlin_keep_first_column_comment = true -ij_kotlin_keep_indents_on_empty_lines = false -ij_kotlin_keep_line_breaks = true -ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_comment_add_space = false -ij_kotlin_line_comment_at_first_column = true -ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = true -ij_kotlin_method_parameters_right_paren_on_new_line = true -ij_kotlin_method_parameters_wrap = on_every_item -ij_kotlin_name_count_to_use_star_import = 9999 -ij_kotlin_name_count_to_use_star_import_for_members = 9999 -ij_kotlin_parameter_annotation_wrap = off -ij_kotlin_space_after_comma = true -ij_kotlin_space_after_extend_colon = true -ij_kotlin_space_after_type_colon = true -ij_kotlin_space_before_catch_parentheses = true -ij_kotlin_space_before_comma = false -ij_kotlin_space_before_extend_colon = true -ij_kotlin_space_before_for_parentheses = true -ij_kotlin_space_before_if_parentheses = true -ij_kotlin_space_before_lambda_arrow = true -ij_kotlin_space_before_type_colon = false -ij_kotlin_space_before_when_parentheses = true -ij_kotlin_space_before_while_parentheses = true -ij_kotlin_spaces_around_additive_operators = true -ij_kotlin_spaces_around_assignment_operators = true -ij_kotlin_spaces_around_equality_operators = true -ij_kotlin_spaces_around_function_type_arrow = true -ij_kotlin_spaces_around_logical_operators = true -ij_kotlin_spaces_around_multiplicative_operators = true -ij_kotlin_spaces_around_range = false -ij_kotlin_spaces_around_relational_operators = true -ij_kotlin_spaces_around_unary_operator = false -ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_variable_annotation_wrap = off -ij_kotlin_while_on_new_line = false -ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_continuation_indent_size = 2 -ij_xml_align_attributes = false -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_add_space = false -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = true -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = false -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = true +[go.{mod,work,work.sum,sum}] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset +indent_size = unset -[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] -ij_json_array_wrapping = split_into_lines -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_keep_trailing_comma = false -ij_json_object_wrapping = split_into_lines -ij_json_property_alignment = do_not_align -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = false -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_add_space = false -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal - -[{*.markdown,*.md}] +[*.md] indent_size = 4 -tab_width = 4 -ij_continuation_indent_size = 8 -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_insert_quote_arrows_on_wrap = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_keep_line_breaks_inside_text_blocks = true -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 -ij_markdown_wrap_text_if_long = true -ij_markdown_wrap_text_inside_blockquotes = true +trim_trailing_whitespace = false + +eclint_indent_style = unset -[{*.yaml,*.yml}] -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true +[Dockerfile] +indent_size = 4 \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 3227fba..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,174 +0,0 @@ -name: CI Pipeline - -on: - push: - branches: - - main - pull_request: - branches: - - "**" - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Build Project - run: ./gradlew assemble - env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" - - pmd: - name: PMD Analysis - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run PMD Analysis - run: ./gradlew pmdMain - - checkstyle: - name: Checkstyle Analysis - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run Checkstyle - run: ./gradlew checkstyleMain - - unit-test: - name: Unit Tests - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run Unit Tests - run: ./gradlew test - - integration-test: - name: Integration Tests - runs-on: ubuntu-latest - needs: build - # Do not run since integration tests are not working - if: false - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run Integration Tests - run: ./gradlew integrationTest - - uses: actions/upload-artifact@v4 # upload test results - if: success() || failure() # run this step even if previous step failed - with: - name: test-results-integration - path: build/reports/tests/integrationTest/index.html - - # Combine test reports if needed - test-report: - name: Test Report - runs-on: ubuntu-latest - # Do not run since integration tests are not working - if: false - needs: [unit-test, integration-test] - steps: - - uses: dorny/test-reporter@v1 - with: - artifact: /test-results-(.*)/ # artifact name - name: java-junit # Name of the check run which will be created - path: "*.html" # Path to test results (inside artifact .zip) - reporter: jest-junit # Format of test results - - test: - name: test end node - runs-on: ubuntu-latest - needs: [build, pmd, checkstyle, unit-test] - steps: - - run: echo "finished test stage" # this is a indirection to the previous test steps - - docker-image-native: - name: Docker image native - if: github.event_name == 'push' && github.ref_name == 'main' - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Build and Push Image - uses: explorviz/deployment/.github/actions/build-and-deploy-quarkus-native@main - with: - platforms: "linux/amd64,linux/arm64/v8" - docker-username: ${{ secrets.DOCKER_USERNAME }} - docker-password: ${{ secrets.DOCKER_PASSWORD }} - image-name: ${{ vars.DOCKER_NATIVE_IMAGE_NAME }} - image-tag: "latest" - - docker-image-jvm: - name: Docker image JVM - if: github.event_name == 'push' && github.ref_name == 'main' - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Build and Push Image - uses: explorviz/deployment/.github/actions/build-and-deploy-quarkus-jvm@main - with: - platforms: "linux/amd64,linux/arm64/v8" - docker-username: ${{ secrets.DOCKER_USERNAME }} - docker-password: ${{ secrets.DOCKER_PASSWORD }} - image-name: ${{ vars.DOCKER_JVM_IMAGE_NAME }} - image-tag: "latest" diff --git a/.github/workflows/deploy-docker.yaml b/.github/workflows/deploy-docker.yaml new file mode 100644 index 0000000..145de6e --- /dev/null +++ b/.github/workflows/deploy-docker.yaml @@ -0,0 +1,86 @@ +name: CI Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + workflow_dispatch: + +env: + GO_VERSION: "1.26.x" + GOLANGCI_LINT_VERSION: "v2.12" + BUF_VERSION: "1.70.0" + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + - name: Install dependencies + run: go get ./... + - name: Build + run: go build -v ./... + - name: Test + run: go test -v -race + + lint: + name: Code Lint + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: ${{ env.GOLANGCI_LINT_VERSION }} + + buf: + name: Protobuf Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: bufbuild/buf-action@v1 + with: + version: ${{ env.BUF_VERSION }} + breaking: false + push: false + archive: false + pr_comment: false + + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [test, lint, buf] + + steps: + - uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to Docker Container Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build and push multi-arch Docker image + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ vars.DOCKER_IMAGE_NAME }}:latest . \ + --push \ No newline at end of file diff --git a/.github/workflows/release-version.yml b/.github/workflows/release-version.yml deleted file mode 100644 index dfa41e5..0000000 --- a/.github/workflows/release-version.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Release Tagged Docker Image - -on: - push: - tags: - - "*" - workflow_dispatch: - -jobs: - release-docker-image-jvm: - name: Release Tagged Docker Image - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Build and Push Image - uses: explorviz/deployment/.github/actions/build-and-deploy-quarkus-jvm@main - with: - platforms: "linux/amd64,linux/arm64/v8" - docker-username: ${{ secrets.DOCKER_USERNAME }} - docker-password: ${{ secrets.DOCKER_PASSWORD }} - image-name: ${{ vars.DOCKER_JVM_IMAGE_NAME }} - image-tag: "${{ github.ref_name }}" diff --git a/.gitignore b/.gitignore index 2c1696e..0589a43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,310 +1,32 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/intellij,eclipse,java,gradle,macos,windows,linux -# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,eclipse,java,gradle,macos,windows,linux - -target -.idea - -### Eclipse ### -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# CDT- autotools -.autotools - -# Java annotation processor (APT) -.factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Annotation Processing -.apt_generated/ -.apt_generated_test/ - -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - -# Uncomment this line if you wish to ignore the project description file. -# Typically, this file would be tracked if it contains build/dependency configurations: -#.project - -### Eclipse Patch ### -# Spring Boot Tooling -.sts4-cache/ - -### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf -*.hprof - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -### Java ### -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -### Gradle ### -.gradle -**/build/ -!src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Avoid ignore Gradle wrappper properties -!gradle-wrapper.properties - -# Cache of project -.gradletasknamecache - -# Eclipse Gradle plugin generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - -# End of https://www.toptal.com/developers/gitignore/api/intellij,eclipse,java,gradle,macos,windows,linux - -# Quarkus -.quarkus - -# Kotlin -.kotlin - +# Based on: +# https://github.com/github/gitignore/blob/main/Go.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +.idea/ +.vscode/ + +# Project-specific +trace-service \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e3ea0da..7a67ef7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,19 +2,14 @@ include: - project: "ExplorViz/code/deployment" file: ".gitlab-ci-explorviz-common.yml" +variables: + GO_DOCKER_VERSION: "1.26" + GOLANGCI_LINT_VERSION: "v2.12" + BUF_VERSION: "1.71" + default: tags: - exec-docker - image: ghcr.io/graalvm/graalvm-community:21 - cache: # Cache modules in between jobs - key: $CI_COMMIT_REF_SLUG - paths: - - .gradle - before_script: - - export GRADLE_USER_HOME=`pwd`/.gradle - -variables: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" workflow: rules: @@ -29,54 +24,66 @@ stages: - test - deploy:images +# See https://docs.gitlab.com/ci/caching/examples/#go +.go-cache: + variables: + GOPATH: $CI_PROJECT_DIR/.go + GOCACHE: "$CI_PROJECT_DIR/.cache/go-build" + before_script: + - mkdir -p .go .cache/go-build + cache: + key: + files: + - go.mod + - go.sum + paths: + - .go/pkg/mod/ + - .cache/go-build/ + build: + image: "golang:${GO_DOCKER_VERSION}" + extends: .go-cache stage: build - script: ./gradlew assemble + script: + - go build -o trace-service . artifacts: paths: - - build + - ./trace-service -pmd: +code-lint: + image: "golangci/golangci-lint:${GOLANGCI_LINT_VERSION}" + extends: .go-cache stage: code-analysis - script: ./gradlew pmdMain + script: + - golangci-lint run ./... --timeout=5m --output.code-climate.path=gl-code-quality-report.json artifacts: - paths: - - build + reports: + codequality: gl-code-quality-report.json -checkstyle: +proto-lint: + image: + name: "bufbuild/buf:${BUF_VERSION}" + entrypoint: [""] stage: code-analysis - script: ./gradlew checkstyleMain - artifacts: - paths: - - build - -unit-test: - stage: test + rules: + - changes: + - "**/*.proto" + - ./proto/buf.yaml script: - - ./gradlew test - artifacts: - paths: - - build + - buf lint -integration-test: +test: + image: "golang:${GO_DOCKER_VERSION}" + extends: .go-cache stage: test script: - - ./gradlew integrationTest - allow_failure: true - artifacts: - paths: - - build - -push-dockerhub-native-mr: - extends: .build-and-deploy-quarkus-native - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - variables: - IMAGE_TAG: mr-${CI_MERGE_REQUEST_IID}-native + - go test -v -race ./... push-dockerhub-mr: - extends: .build-and-deploy-quarkus-jvm + extends: .build-and-deploy rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: - IMAGE_TAG: mr-${CI_MERGE_REQUEST_IID}-jvm + DOCKERHUB_IMAGE_NAME: "trace-service" + + IMAGE_TAG: mr-${CI_MERGE_REQUEST_IID} diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a0338a7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +version: "2" + +linters: + default: none + + enable: + - errcheck + - govet + - protogetter + - staticcheck + - unused + +formatters: + enable: + - goimports + + settings: + goimports: + local-prefixes: + - github.com/ExplorViz/trace-service diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46611e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.26 AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /trace-service -ldflags="-s -w" + +FROM scratch + +COPY --from=builder /trace-service /trace-service + +EXPOSE 8081 + +ENTRYPOINT ["/trace-service"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..61b0f87 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: all generate build run + +all: generate build + +generate: + go generate + +build: generate + go build + +run: generate + @go run . || true \ No newline at end of file diff --git a/README.md b/README.md index 3e7313d..2cc4d80 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,68 @@ -# ExplorViz trace-service +# trace-service -Scalable service that processes, persists, aggregates and queries the observed traces of method executions within -monitored software applications. +The trace-service is a scalable service that processes, interprets, persists, and queries [OpenTelemetry execution traces](https://opentelemetry.io/docs/concepts/signals/traces/) within monitored software applications. It attempts to classify the entities described by incoming spans for the purpose of visualization. -## Prerequisites +Consumes traces sent via Kafka from an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) instance (defined in the [Deployment](../deployment)). Processed spans are produced to a Kafka topic for the [Landscape Service](../landscape-service) to consume. Interaction with Kafka is implemented using the [franz-go](https://github.com/twmb/franz-go) library. -- Java 17 or higher -- Make sure to run the [ExplorViz software stack](https://git.se.informatik.uni-kiel.de/ExplorViz/code/deployment) +For development instructions, continue reading below. If you just want to run ExplorViz locally, refer to our [Deployment repository](../deployment) instead. + +## Development Instructions + +### Prerequisites + +- Go 1.25.10 or higher +- A code editor, such as [Visual Studio Code](https://code.visualstudio.com/) +- Make sure to run the [ExplorViz software stack](../deployment) before starting the service, as it provides the required database(s) and the Kafka broker -## Running the application in dev mode +### Running the service -You can run your application in dev mode that enables live coding using: -```shell script -./gradlew quarkusDev +You can run the service using: + +```shell +go run . [OPTIONS] ``` -This also enables the `dev` configuration profile, i.e. using the properties prefixed with `%dev` from -`src/main/resources/application.properties`. +To see a list of command-line options, use the `--help` flag. These options can also be configured via environment variables, where the name of the environment variable corresponds to the long flag name, prefixed by `EXPLORVIZ_` and with all separators replaced by underscores; for example, the `--log-level` flag corresponds to the `EXPLORVIZ_LOG_LEVEL` environment variable. Note that directly passing flags takes precedence over environment variables. If neither the flag nor the environment variable is set, then the default value indicated by `--help` is used. -**_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. +### Building an executable -## Packaging and running the application +To build an executable from the project, use: -The application can be packaged and tested using: -```shell script -./gradlew build +```shell +go build ``` -It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. -You can skip running the integration tests by adding `-x integrationTest`. To skip all tests and code analysis use the -`assemble` task instead of `build`. +By default, the executable will be placed in the root directory under the name `trace-service`. You can optionally specify the path of the resulting binary using the `-o ` flag. + +### Testing -The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. -Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. +Be sure to write tests for new code and ensure that existing tests pass. You can run all tests using: -If you want to build an _über-jar_, which includes the entire application in a single jar file, execute the following -command: -```shell script -./gradlew build -Dquarkus.package.type=uber-jar +```shell +go test ./... ``` -The application, packaged as an _über-jar_, is now runnable using -`java -jar build/trace-service-1.0-SNAPSHOT-runner.jar`. -You can add `-Dquarkus.profile=dev` to enable the `%dev` properties. +### Compiling Protobuf -## Creating a native executable +When updating any `.proto` files, make sure to compile the Protobuf files to Go using: -You can create a native executable using: -```shell script -./gradlew build -Dquarkus.package.type=native +```shell +go generate ``` -Or, if you don't have GraalVM installed, you can run the native executable build in a container using: -```shell script -./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +Alternatively, you can use the provided Makefile to compile the Protobuf and build the project in a single step: + +```shell +make +``` + +If you just want to run the project while also compiling the Protobuf, use: + +```shell +make run ``` -You can then execute your native executable with: `./build/trace-service-1.0-SNAPSHOT-runner` +### Code Style -If you want to learn more about building native executables, please consult -https://quarkus.io/guides/gradle-tooling#building-a-native-executable. +As part of our CI/CD pipeline, your code is linted and checked for formatting using [golangci-lint](https://github.com/golangci/golangci-lint), which you can also install locally to lint your code yourself prior to pushing. We recommend using the [official Visual Studio Code extension for Go](https://marketplace.visualstudio.com/items?itemName=golang.go) as well as [configuring the extension for golangci-lint](https://golangci-lint.run/docs/welcome/integrations/) to detect and fix linting / formatting issues as you're working. diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..0d2aeba --- /dev/null +++ b/buf.yaml @@ -0,0 +1,4 @@ +# Buf is used for CI/CD linting of .proto files +version: v2 +modules: + - path: . diff --git a/build.gradle b/build.gradle deleted file mode 100644 index ec5e38e..0000000 --- a/build.gradle +++ /dev/null @@ -1,200 +0,0 @@ -plugins { - id 'java' - id 'io.quarkus' - - id 'com.adarshr.test-logger' version '3.2.0' - id "org.kordamp.gradle.jandex" version "2.0.0" - - id "org.jetbrains.kotlin.jvm" version "2.0.21" - id "org.jetbrains.kotlin.plugin.allopen" version "2.0.21" - - id "io.gitlab.arturbosch.detekt" version "1.23.7" - id "com.diffplug.spotless" version "6.25.0" -} - -apply from: 'code-analysis/code-analysis.gradle' - -repositories { - mavenCentral() - mavenLocal() - maven { - url "https://packages.confluent.io/maven/" - } -} - -dependencies { - implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") - - implementation("io.quarkus:quarkus-vertx-http") - - implementation 'io.quarkus:quarkus-container-image-jib' - - implementation "io.quarkus:quarkus-kotlin" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - - // Needed for scheduled jobs - implementation 'io.quarkus:quarkus-scheduler' - - implementation "io.opentelemetry.proto:opentelemetry-proto:0.19.0-alpha" - - // Kafka Streams + Confluent Schema Registry + Avro (de)serializer - implementation 'io.quarkus:quarkus-kafka-streams' - implementation 'io.quarkus:quarkus-confluent-registry-avro' - implementation group: 'io.confluent', name: 'kafka-streams-avro-serde', version: "${confluentAvroSerdeVersion}" - implementation 'io.quarkus:quarkus-messaging-kafka' - - //implementation("org.jboss.slf4j:slf4j-jboss-logmanager") - //testImplementation("org.jboss.slf4j:slf4j-jboss-logmanager") - //integrationTestImplementation("org.jboss.slf4j:slf4j-jboss-logmanager") - - testImplementation 'io.quarkus:quarkus-junit5' - testImplementation 'io.quarkus:quarkus-junit5-mockito' - - // Integration / Api Tests - testImplementation "org.testcontainers:kafka:1.17.5" - testImplementation group: "org.apache.kafka", name: "kafka-streams-test-utils", version: "3.3.1" - integrationTestImplementation "org.testcontainers:kafka:1.17.5" - - // Integration Testing - integrationTestImplementation 'io.quarkus:quarkus-junit5' -} - -group 'net.explorviz' -version '1.0-SNAPSHOT' - -compileJava { - options.encoding = 'UTF-8' - options.compilerArgs << '-parameters' -} - -compileTestJava { - options.encoding = 'UTF-8' -} - -kotlin { - jvmToolchain(21) -} - -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -// Tasks Setups // - -sourceSets { - main { - java { - srcDirs 'build/classes/java/quarkus-generated-sources/grpc' - } - } - - integrationTest { - java.srcDir file('src/integrationTest/java') - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - } -} - -task integrationTest(type: Test) { - description = 'Runs integration tests.' - group = 'verification' - - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - shouldRunAfter test -} - -configurations { - integrationTestImplementation.extendsFrom implementation - integrationTestRuntimeOnly.extendsFrom runtimeOnly - - integrationTestImplementation.setCanBeResolved(true) - integrationTestRuntimeOnly.setCanBeResolved(true) -} - -// Regarding jandex dependency for each task -// https://github.com/kordamp/jandex-gradle-plugin/issues/9 - -tasks.named('imageBuild') { - dependsOn 'jandex' -} - -tasks.named('imagePush') { - dependsOn 'jandex' -} - -tasks.named('checkstyleMain') { - dependsOn 'jandex' -} - -tasks.named('pmdMain') { - dependsOn 'jandex' -} - -tasks.named('compileTestJava') { - dependsOn 'jandex' -} - -tasks.named('test') { - dependsOn 'jandex' -} - -tasks.named('compileIntegrationTestJava') { - dependsOn 'jandex' -} - -tasks.named('integrationTest') { - dependsOn 'jandex' -} - -tasks.named('quarkusDependenciesBuild') { - dependsOn 'jandex' -} - -spotless { - kotlin { - ktfmt().kotlinlangStyle().configure { - it.setMaxWidth(120) - } - } -} - - -tasks.withType(JavaCompile).configureEach { - // Turn on all javac warnings except classfile and processing, which produces many false-positives about annotations - options.compilerArgs << "-Xlint:all" << "-Xlint:-classfile" << "-Xlint:-processing" -} - -test { - useJUnitPlatform() - testLogging.showStandardStreams = true -} - -// Needed by Mockito since closed Kotlin classes cannot be mocked -allOpen { - annotation("jakarta.enterprise.context.ApplicationScoped") - annotation("jakarta.persistence.Entity") - annotation("io.quarkus.test.junit.QuarkusTest") -} - -integrationTest { - useJUnitPlatform() - testLogging.showStandardStreams = true -} - -def registerGitHook = { String taskName, String hookFile, String targetHook -> - tasks.register(taskName, Copy) { - from("code-analysis/${hookFile}") - into(".git/hooks") - rename { targetHook } - } -} - -registerGitHook("registerPreCommitHook", "pre-commit", "pre-commit") -registerGitHook("registerPreMergeCommitHook", "pre-commit", "pre-merge-commit") - -tasks.named("quarkusGenerateCode") { - dependsOn("registerPreCommitHook") - dependsOn("registerPreMergeCommitHook") -} diff --git a/code-analysis/checkstyle-suppression.xml b/code-analysis/checkstyle-suppression.xml deleted file mode 100644 index 8b5eea5..0000000 --- a/code-analysis/checkstyle-suppression.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/code-analysis/checkstyle.xml b/code-analysis/checkstyle.xml deleted file mode 100644 index dfc9f16..0000000 --- a/code-analysis/checkstyle.xml +++ /dev/null @@ -1,381 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/code-analysis/code-analysis.gradle b/code-analysis/code-analysis.gradle deleted file mode 100644 index 74b2b51..0000000 --- a/code-analysis/code-analysis.gradle +++ /dev/null @@ -1,30 +0,0 @@ -apply plugin: 'pmd' -apply plugin: 'checkstyle' -apply plugin: 'jacoco' - -jacocoTestReport { - - executionData { tasks.withType(Test).findAll { it.jacoco.destinationFile.exists() }*.jacoco.destinationFile } - - reports { - xml.required = true - html.required = true - } -} - -pmd { - // Empty ruleset is necessary for exclude-pattern - // https://stackoverflow.com/questions/32247190/pmd-exclude-pattern-with-gradle - ruleSets = [] - ruleSetFiles = files("code-analysis/pmd.xml") - ignoreFailures = false - toolVersion = "6.53.0" -} - -checkstyle { - configDirectory = file("code-analysis") - configFile = file("code-analysis/checkstyle.xml") - maxWarnings = 0 - ignoreFailures = false - toolVersion = "10.12.5" -} diff --git a/code-analysis/pmd.xml b/code-analysis/pmd.xml deleted file mode 100644 index 38ab891..0000000 --- a/code-analysis/pmd.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - CAU-SE PMD ruleset - - .*/io.opencensus/.* - .*/build/.* - .*/src/test/.* - .*/src/integrationTest/.* - .*/src/main/java/net/explorviz/span/hash/HighwayHash.java - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/code-analysis/pre-commit b/code-analysis/pre-commit deleted file mode 100755 index 4b4bf60..0000000 --- a/code-analysis/pre-commit +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -echo "Running pre-commit hook" - -./gradlew checkstyleMain pmdMain test -if [ $? -ne 0 ]; then - echo "Pre-commit hook failed" - exit 1 -fi - -echo "Pre-commit hook passed" -exit 0 diff --git a/githooks/pre-commit b/githooks/pre-commit new file mode 100755 index 0000000..d4a0b3d --- /dev/null +++ b/githooks/pre-commit @@ -0,0 +1,39 @@ +#!/bin/bash + +cleanup() { + local result=$? + + if [ $result -eq 1 ] + then + echo "Pre-commit hook failed." + else + echo "Pre-commit hook passed." + fi + + exit $result +} + +trap cleanup EXIT + +echo "Running pre-commit hook" +echo +echo "NOTE: The pre-commit hook runs on all files in the working tree, +not just on staged changes. If this leads to false positives, you can +stash your unstaged changes before committing again." +echo + +if ! command -v go &> /dev/null; then + echo "Command \"go\" is not available." + echo "Make sure to install it or bypass the hook via the --no-verify flag" + exit 1 +fi + + +if ! command -v golangci-lint &> /dev/null; then + echo "Command \"golangci-lint\" is not available." + echo "Make sure to install it or bypass the hook via the --no-verify flag" + exit 1 +fi + +golangci-lint run || { echo "There are linting issues present."; exit 1; } +go test ./... || { echo "There are failing tests."; exit 1; } \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b225236 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/ExplorViz/trace-service + +go 1.25.10 + +require ( + github.com/peterbourgon/ff/v4 v4.0.0-beta.1 + github.com/twmb/franz-go v1.21.3 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/proto/otlp v1.10.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..51ed346 --- /dev/null +++ b/go.sum @@ -0,0 +1,68 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc= +github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twmb/franz-go v1.21.3 h1:q9Mo8ri+OwBQBjKqrerNCqNWJlJnUDe2qnYsj2V3hdI= +github.com/twmb/franz-go v1.21.3/go.mod h1:rfoMTnVk7107fhTGxfEKIHP/e7tPe6oyij/ywzO0czk= +github.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI= +github.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 8b32b51..0000000 --- a/gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -#Gradle properties -quarkusPluginVersion=3.21.2 -quarkusPlatformArtifactId=quarkus-bom -quarkusPluginId=io.quarkus -quarkusPlatformGroupId=io.quarkus.platform -quarkusPlatformVersion=3.21.2 -confluentAvroSerdeVersion=7.3.3 - -# Gradle daemon memory settings -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# Kotlin daemon memory settings -kotlin.daemon.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 62d4c05..0000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a595206..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index fbd7c51..0000000 --- a/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 5093609..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,104 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/internal/attrib/attributes.go b/internal/attrib/attributes.go new file mode 100644 index 0000000..882ada6 --- /dev/null +++ b/internal/attrib/attributes.go @@ -0,0 +1,31 @@ +// Package attrib is concerned with OpenTelemetry attributes. +// It defines ExplorViz-specific attributes and provides utilities +// for efficiently retrieving attributes from OTLP data. +package attrib + +import "go.opentelemetry.io/otel/attribute" + +type ExplorVizAttribute struct { + Key attribute.Key + DefaultValue string +} + +// ExplorVizAttributes defines attributes required or written by ExplorViz itself. +var ExplorVizAttributes = struct { + LandscapeTokenID ExplorVizAttribute + LandscapeTokenSecret ExplorVizAttribute + EntityId ExplorVizAttribute +}{ + LandscapeTokenID: ExplorVizAttribute{ + Key: "explorviz.token.id", + DefaultValue: "mytokenvalue", + }, + LandscapeTokenSecret: ExplorVizAttribute{ + Key: "explorviz.token.secret", + DefaultValue: "mytokenvalue", + }, + EntityId: ExplorVizAttribute{ + Key: "explorviz.entity.id", + DefaultValue: "unknown", + }, +} diff --git a/internal/attrib/spanreader.go b/internal/attrib/spanreader.go new file mode 100644 index 0000000..b9de6d9 --- /dev/null +++ b/internal/attrib/spanreader.go @@ -0,0 +1,82 @@ +package attrib + +import ( + "encoding/hex" + + "go.opentelemetry.io/otel/attribute" + + commonpb "go.opentelemetry.io/proto/otlp/common/v1" + resourcepb "go.opentelemetry.io/proto/otlp/resource/v1" + tracepb "go.opentelemetry.io/proto/otlp/trace/v1" +) + +// A SpanReader groups a Protobuf [tracepb.Span] together with its [commonpb.InstrumentationScope] +// and [resourcepb.Resource]. It provides helper methods for efficient lookup of attributes +// by leveraging pre-constructed maps. +type SpanReader struct { + Span *tracepb.Span + Scope *commonpb.InstrumentationScope + Resource *resourcepb.Resource + + spanAttributes map[string]*commonpb.AnyValue + scopeAttributes map[string]*commonpb.AnyValue + resourceAttributes map[string]*commonpb.AnyValue +} + +func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resourcepb.Resource) SpanReader { + return SpanReader{ + Span: s, + Scope: sc, + Resource: rs, + + spanAttributes: attrsToMap(s.GetAttributes()), + scopeAttributes: attrsToMap(sc.GetAttributes()), + resourceAttributes: attrsToMap(rs.GetAttributes()), + } +} + +func (sr SpanReader) SpanAttribute(key attribute.Key) *commonpb.AnyValue { + return sr.spanAttributes[string(key)] +} + +func (sr SpanReader) ScopeAttribute(key attribute.Key) *commonpb.AnyValue { + return sr.scopeAttributes[string(key)] +} + +func (sr SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { + return sr.resourceAttributes[string(key)] +} + +func (sr SpanReader) TraceID() string { + return hex.EncodeToString(sr.Span.GetTraceId()) +} + +func (sr SpanReader) SpanID() string { + return hex.EncodeToString(sr.Span.GetSpanId()) +} + +func (sr SpanReader) ParentSpanID() string { + return hex.EncodeToString(sr.Span.GetParentSpanId()) +} + +func (sr SpanReader) TokenID() string { + if v := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue(); v != "" { + return v + } + return sr.SpanAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() +} + +func (sr SpanReader) TokenSecret() string { + if v := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue(); v != "" { + return v + } + return sr.SpanAttribute(ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() +} + +func attrsToMap(attrs []*commonpb.KeyValue) map[string]*commonpb.AnyValue { + m := make(map[string]*commonpb.AnyValue, len(attrs)) + for _, kv := range attrs { + m[kv.GetKey()] = kv.GetValue() + } + return m +} diff --git a/internal/conversion/protobuf.go b/internal/conversion/protobuf.go new file mode 100644 index 0000000..efd67c8 --- /dev/null +++ b/internal/conversion/protobuf.go @@ -0,0 +1,47 @@ +package conversion + +import ( + "github.com/ExplorViz/trace-service/internal/genproto/spanpb" + "github.com/ExplorViz/trace-service/internal/parsing" +) + +func ToProto(ps ParsedSpan) *spanpb.ParsedSpan { + s := spanpb.ParsedSpan{ + LandscapeTokenId: ps.LandscapeTokenId, + LandscapeTokenSecret: ps.LandscapeTokenSecret, + + TraceId: ps.TraceId, + SpanId: ps.SpanId, + SpanName: ps.SpanName, + ParentId: strOrNil(ps.ParentSpanId), + + StartTime: ps.StartTime, + EndTime: ps.EndTime, + + ApplicationName: ps.ApplicationName, + + EntityId: ps.Entity.Id(), + } + + switch e := ps.Entity.(type) { + case parsing.CodeSpanEntity: + s.EntityDescriptor = &spanpb.ParsedSpan_CodeDescriptor{ + CodeDescriptor: &spanpb.CodeDescriptor{ + FilePath: e.FilePath, + FunctionName: e.FuncName, + ClassName: strOrNil(e.ClassName), + Language: strOrNil(e.Language), + GitCommitHash: strOrNil(e.GitCommitHash), + }, + } + } + + return &s +} + +func strOrNil(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/internal/conversion/span.go b/internal/conversion/span.go new file mode 100644 index 0000000..a165648 --- /dev/null +++ b/internal/conversion/span.go @@ -0,0 +1,60 @@ +package conversion + +import ( + "errors" + "log/slog" + + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" + + "github.com/ExplorViz/trace-service/internal/attrib" + "github.com/ExplorViz/trace-service/internal/parsing" +) + +type ParsedSpan struct { + LandscapeTokenId string + LandscapeTokenSecret string + + TraceId string + SpanId string + SpanName string + ParentSpanId string + + StartTime uint64 + EndTime uint64 + + ApplicationName string + Entity parsing.SpanEntity +} + +var parserChain = []parsing.SpanParser{ + parsing.ParseCodeSpan, +} + +func ConvertSpan(sr *attrib.SpanReader) (ParsedSpan, error) { + tokenId := sr.ResourceAttribute(attrib.ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() + tokenSecret := sr.ResourceAttribute(attrib.ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() + appName := sr.ResourceAttribute(semconv.ServiceNameKey).GetStringValue() + + for _, parser := range parserChain { + entity, err := parser(sr) + if err != nil { + slog.Debug("parser failed", "error", err) + continue + } + return ParsedSpan{ + LandscapeTokenId: tokenId, + LandscapeTokenSecret: tokenSecret, + + TraceId: sr.TraceID(), + SpanId: sr.SpanID(), + ParentSpanId: sr.ParentSpanID(), + + StartTime: sr.Span.GetStartTimeUnixNano(), + EndTime: sr.Span.GetEndTimeUnixNano(), + + ApplicationName: appName, + Entity: entity, + }, nil + } + return ParsedSpan{}, errors.New("no matching span parser found") +} diff --git a/internal/genproto/spanpb/parsed_span.pb.go b/internal/genproto/spanpb/parsed_span.pb.go new file mode 100644 index 0000000..5c8946b --- /dev/null +++ b/internal/genproto/spanpb/parsed_span.pb.go @@ -0,0 +1,332 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.19.6 +// source: parsed_span.proto + +package spanpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ParsedSpan struct { + state protoimpl.MessageState `protogen:"open.v1"` + LandscapeTokenId string `protobuf:"bytes,1,opt,name=landscape_token_id,json=landscapeTokenId,proto3" json:"landscape_token_id,omitempty"` + LandscapeTokenSecret string `protobuf:"bytes,2,opt,name=landscape_token_secret,json=landscapeTokenSecret,proto3" json:"landscape_token_secret,omitempty"` + TraceId string `protobuf:"bytes,3,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + SpanId string `protobuf:"bytes,4,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` + SpanName string `protobuf:"bytes,5,opt,name=span_name,json=spanName,proto3" json:"span_name,omitempty"` + ParentId *string `protobuf:"bytes,6,opt,name=parent_id,json=parentId,proto3,oneof" json:"parent_id,omitempty"` + StartTime uint64 `protobuf:"varint,7,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime uint64 `protobuf:"varint,8,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + ApplicationName string `protobuf:"bytes,9,opt,name=application_name,json=applicationName,proto3" json:"application_name,omitempty"` + EntityId string `protobuf:"bytes,10,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` // Identifier for the entity described by the span (e.g. function, database, ...) + // Types that are valid to be assigned to EntityDescriptor: + // + // *ParsedSpan_CodeDescriptor + EntityDescriptor isParsedSpan_EntityDescriptor `protobuf_oneof:"entity_descriptor"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParsedSpan) Reset() { + *x = ParsedSpan{} + mi := &file_parsed_span_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParsedSpan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParsedSpan) ProtoMessage() {} + +func (x *ParsedSpan) ProtoReflect() protoreflect.Message { + mi := &file_parsed_span_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParsedSpan.ProtoReflect.Descriptor instead. +func (*ParsedSpan) Descriptor() ([]byte, []int) { + return file_parsed_span_proto_rawDescGZIP(), []int{0} +} + +func (x *ParsedSpan) GetLandscapeTokenId() string { + if x != nil { + return x.LandscapeTokenId + } + return "" +} + +func (x *ParsedSpan) GetLandscapeTokenSecret() string { + if x != nil { + return x.LandscapeTokenSecret + } + return "" +} + +func (x *ParsedSpan) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +func (x *ParsedSpan) GetSpanId() string { + if x != nil { + return x.SpanId + } + return "" +} + +func (x *ParsedSpan) GetSpanName() string { + if x != nil { + return x.SpanName + } + return "" +} + +func (x *ParsedSpan) GetParentId() string { + if x != nil && x.ParentId != nil { + return *x.ParentId + } + return "" +} + +func (x *ParsedSpan) GetStartTime() uint64 { + if x != nil { + return x.StartTime + } + return 0 +} + +func (x *ParsedSpan) GetEndTime() uint64 { + if x != nil { + return x.EndTime + } + return 0 +} + +func (x *ParsedSpan) GetApplicationName() string { + if x != nil { + return x.ApplicationName + } + return "" +} + +func (x *ParsedSpan) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +func (x *ParsedSpan) GetEntityDescriptor() isParsedSpan_EntityDescriptor { + if x != nil { + return x.EntityDescriptor + } + return nil +} + +func (x *ParsedSpan) GetCodeDescriptor() *CodeDescriptor { + if x != nil { + if x, ok := x.EntityDescriptor.(*ParsedSpan_CodeDescriptor); ok { + return x.CodeDescriptor + } + } + return nil +} + +type isParsedSpan_EntityDescriptor interface { + isParsedSpan_EntityDescriptor() +} + +type ParsedSpan_CodeDescriptor struct { + CodeDescriptor *CodeDescriptor `protobuf:"bytes,11,opt,name=code_descriptor,json=codeDescriptor,proto3,oneof"` +} + +func (*ParsedSpan_CodeDescriptor) isParsedSpan_EntityDescriptor() {} + +type CodeDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + FilePath string `protobuf:"bytes,1,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + FunctionName string `protobuf:"bytes,2,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` + ClassName *string `protobuf:"bytes,3,opt,name=class_name,json=className,proto3,oneof" json:"class_name,omitempty"` // Inner classes may be separated by "." + Language *string `protobuf:"bytes,4,opt,name=language,proto3,oneof" json:"language,omitempty"` + GitCommitHash *string `protobuf:"bytes,5,opt,name=git_commit_hash,json=gitCommitHash,proto3,oneof" json:"git_commit_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodeDescriptor) Reset() { + *x = CodeDescriptor{} + mi := &file_parsed_span_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodeDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodeDescriptor) ProtoMessage() {} + +func (x *CodeDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_parsed_span_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodeDescriptor.ProtoReflect.Descriptor instead. +func (*CodeDescriptor) Descriptor() ([]byte, []int) { + return file_parsed_span_proto_rawDescGZIP(), []int{1} +} + +func (x *CodeDescriptor) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *CodeDescriptor) GetFunctionName() string { + if x != nil { + return x.FunctionName + } + return "" +} + +func (x *CodeDescriptor) GetClassName() string { + if x != nil && x.ClassName != nil { + return *x.ClassName + } + return "" +} + +func (x *CodeDescriptor) GetLanguage() string { + if x != nil && x.Language != nil { + return *x.Language + } + return "" +} + +func (x *CodeDescriptor) GetGitCommitHash() string { + if x != nil && x.GitCommitHash != nil { + return *x.GitCommitHash + } + return "" +} + +var File_parsed_span_proto protoreflect.FileDescriptor + +const file_parsed_span_proto_rawDesc = "" + + "\n" + + "\x11parsed_span.proto\"\xc4\x03\n" + + "\n" + + "ParsedSpan\x12,\n" + + "\x12landscape_token_id\x18\x01 \x01(\tR\x10landscapeTokenId\x124\n" + + "\x16landscape_token_secret\x18\x02 \x01(\tR\x14landscapeTokenSecret\x12\x19\n" + + "\btrace_id\x18\x03 \x01(\tR\atraceId\x12\x17\n" + + "\aspan_id\x18\x04 \x01(\tR\x06spanId\x12\x1b\n" + + "\tspan_name\x18\x05 \x01(\tR\bspanName\x12 \n" + + "\tparent_id\x18\x06 \x01(\tH\x01R\bparentId\x88\x01\x01\x12\x1d\n" + + "\n" + + "start_time\x18\a \x01(\x04R\tstartTime\x12\x19\n" + + "\bend_time\x18\b \x01(\x04R\aendTime\x12)\n" + + "\x10application_name\x18\t \x01(\tR\x0fapplicationName\x12\x1b\n" + + "\tentity_id\x18\n" + + " \x01(\tR\bentityId\x12:\n" + + "\x0fcode_descriptor\x18\v \x01(\v2\x0f.CodeDescriptorH\x00R\x0ecodeDescriptorB\x13\n" + + "\x11entity_descriptorB\f\n" + + "\n" + + "_parent_id\"\xf4\x01\n" + + "\x0eCodeDescriptor\x12\x1b\n" + + "\tfile_path\x18\x01 \x01(\tR\bfilePath\x12#\n" + + "\rfunction_name\x18\x02 \x01(\tR\ffunctionName\x12\"\n" + + "\n" + + "class_name\x18\x03 \x01(\tH\x00R\tclassName\x88\x01\x01\x12\x1f\n" + + "\blanguage\x18\x04 \x01(\tH\x01R\blanguage\x88\x01\x01\x12+\n" + + "\x0fgit_commit_hash\x18\x05 \x01(\tH\x02R\rgitCommitHash\x88\x01\x01B\r\n" + + "\v_class_nameB\v\n" + + "\t_languageB\x12\n" + + "\x10_git_commit_hashB=Z;github.com/ExplorViz/trace-service/internal/genproto/spanpbb\x06proto3" + +var ( + file_parsed_span_proto_rawDescOnce sync.Once + file_parsed_span_proto_rawDescData []byte +) + +func file_parsed_span_proto_rawDescGZIP() []byte { + file_parsed_span_proto_rawDescOnce.Do(func() { + file_parsed_span_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_parsed_span_proto_rawDesc), len(file_parsed_span_proto_rawDesc))) + }) + return file_parsed_span_proto_rawDescData +} + +var file_parsed_span_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_parsed_span_proto_goTypes = []any{ + (*ParsedSpan)(nil), // 0: ParsedSpan + (*CodeDescriptor)(nil), // 1: CodeDescriptor +} +var file_parsed_span_proto_depIdxs = []int32{ + 1, // 0: ParsedSpan.code_descriptor:type_name -> CodeDescriptor + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_parsed_span_proto_init() } +func file_parsed_span_proto_init() { + if File_parsed_span_proto != nil { + return + } + file_parsed_span_proto_msgTypes[0].OneofWrappers = []any{ + (*ParsedSpan_CodeDescriptor)(nil), + } + file_parsed_span_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_parsed_span_proto_rawDesc), len(file_parsed_span_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_parsed_span_proto_goTypes, + DependencyIndexes: file_parsed_span_proto_depIdxs, + MessageInfos: file_parsed_span_proto_msgTypes, + }.Build() + File_parsed_span_proto = out.File + file_parsed_span_proto_goTypes = nil + file_parsed_span_proto_depIdxs = nil +} diff --git a/internal/genproto/tokenpb/token_event.pb.go b/internal/genproto/tokenpb/token_event.pb.go new file mode 100644 index 0000000..7fa8711 --- /dev/null +++ b/internal/genproto/tokenpb/token_event.pb.go @@ -0,0 +1,297 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.19.6 +// source: token_event.proto + +package tokenpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EventType int32 + +const ( + EventType_EVENT_TYPE_UNSPECIFIED EventType = 0 + EventType_EVENT_TYPE_CREATED EventType = 1 + EventType_EVENT_TYPE_DELETED EventType = 2 + EventType_EVENT_TYPE_CLONED EventType = 3 + EventType_EVENT_TYPE_ACCESS_GRANTED EventType = 4 + EventType_EVENT_TYPE_ACCESS_REVOKED EventType = 5 +) + +// Enum value maps for EventType. +var ( + EventType_name = map[int32]string{ + 0: "EVENT_TYPE_UNSPECIFIED", + 1: "EVENT_TYPE_CREATED", + 2: "EVENT_TYPE_DELETED", + 3: "EVENT_TYPE_CLONED", + 4: "EVENT_TYPE_ACCESS_GRANTED", + 5: "EVENT_TYPE_ACCESS_REVOKED", + } + EventType_value = map[string]int32{ + "EVENT_TYPE_UNSPECIFIED": 0, + "EVENT_TYPE_CREATED": 1, + "EVENT_TYPE_DELETED": 2, + "EVENT_TYPE_CLONED": 3, + "EVENT_TYPE_ACCESS_GRANTED": 4, + "EVENT_TYPE_ACCESS_REVOKED": 5, + } +) + +func (x EventType) Enum() *EventType { + p := new(EventType) + *p = x + return p +} + +func (x EventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EventType) Descriptor() protoreflect.EnumDescriptor { + return file_token_event_proto_enumTypes[0].Descriptor() +} + +func (EventType) Type() protoreflect.EnumType { + return &file_token_event_proto_enumTypes[0] +} + +func (x EventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EventType.Descriptor instead. +func (EventType) EnumDescriptor() ([]byte, []int) { + return file_token_event_proto_rawDescGZIP(), []int{0} +} + +type TokenEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type EventType `protobuf:"varint,1,opt,name=type,proto3,enum=EventType" json:"type,omitempty"` + Token *LandscapeToken `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + ClonedTokenId *string `protobuf:"bytes,3,opt,name=cloned_token_id,json=clonedTokenId,proto3,oneof" json:"cloned_token_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TokenEvent) Reset() { + *x = TokenEvent{} + mi := &file_token_event_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TokenEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenEvent) ProtoMessage() {} + +func (x *TokenEvent) ProtoReflect() protoreflect.Message { + mi := &file_token_event_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenEvent.ProtoReflect.Descriptor instead. +func (*TokenEvent) Descriptor() ([]byte, []int) { + return file_token_event_proto_rawDescGZIP(), []int{0} +} + +func (x *TokenEvent) GetType() EventType { + if x != nil { + return x.Type + } + return EventType_EVENT_TYPE_UNSPECIFIED +} + +func (x *TokenEvent) GetToken() *LandscapeToken { + if x != nil { + return x.Token + } + return nil +} + +func (x *TokenEvent) GetClonedTokenId() string { + if x != nil && x.ClonedTokenId != nil { + return *x.ClonedTokenId + } + return "" +} + +type LandscapeToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + Alias string `protobuf:"bytes,3,opt,name=alias,proto3" json:"alias,omitempty"` + OwnerId string `protobuf:"bytes,4,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + Created uint64 `protobuf:"varint,5,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LandscapeToken) Reset() { + *x = LandscapeToken{} + mi := &file_token_event_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LandscapeToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LandscapeToken) ProtoMessage() {} + +func (x *LandscapeToken) ProtoReflect() protoreflect.Message { + mi := &file_token_event_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LandscapeToken.ProtoReflect.Descriptor instead. +func (*LandscapeToken) Descriptor() ([]byte, []int) { + return file_token_event_proto_rawDescGZIP(), []int{1} +} + +func (x *LandscapeToken) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *LandscapeToken) GetSecret() string { + if x != nil { + return x.Secret + } + return "" +} + +func (x *LandscapeToken) GetAlias() string { + if x != nil { + return x.Alias + } + return "" +} + +func (x *LandscapeToken) GetOwnerId() string { + if x != nil { + return x.OwnerId + } + return "" +} + +func (x *LandscapeToken) GetCreated() uint64 { + if x != nil { + return x.Created + } + return 0 +} + +var File_token_event_proto protoreflect.FileDescriptor + +const file_token_event_proto_rawDesc = "" + + "\n" + + "\x11token_event.proto\"\x94\x01\n" + + "\n" + + "TokenEvent\x12\x1e\n" + + "\x04type\x18\x01 \x01(\x0e2\n" + + ".EventTypeR\x04type\x12%\n" + + "\x05token\x18\x02 \x01(\v2\x0f.LandscapeTokenR\x05token\x12+\n" + + "\x0fcloned_token_id\x18\x03 \x01(\tH\x00R\rclonedTokenId\x88\x01\x01B\x12\n" + + "\x10_cloned_token_id\"\x83\x01\n" + + "\x0eLandscapeToken\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06secret\x18\x02 \x01(\tR\x06secret\x12\x14\n" + + "\x05alias\x18\x03 \x01(\tR\x05alias\x12\x19\n" + + "\bowner_id\x18\x04 \x01(\tR\aownerId\x12\x18\n" + + "\acreated\x18\x05 \x01(\x04R\acreated*\xac\x01\n" + + "\tEventType\x12\x1a\n" + + "\x16EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12EVENT_TYPE_CREATED\x10\x01\x12\x16\n" + + "\x12EVENT_TYPE_DELETED\x10\x02\x12\x15\n" + + "\x11EVENT_TYPE_CLONED\x10\x03\x12\x1d\n" + + "\x19EVENT_TYPE_ACCESS_GRANTED\x10\x04\x12\x1d\n" + + "\x19EVENT_TYPE_ACCESS_REVOKED\x10\x05B>Z EventType + 2, // 1: TokenEvent.token:type_name -> LandscapeToken + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_token_event_proto_init() } +func file_token_event_proto_init() { + if File_token_event_proto != nil { + return + } + file_token_event_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_token_event_proto_rawDesc), len(file_token_event_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_token_event_proto_goTypes, + DependencyIndexes: file_token_event_proto_depIdxs, + EnumInfos: file_token_event_proto_enumTypes, + MessageInfos: file_token_event_proto_msgTypes, + }.Build() + File_token_event_proto = out.File + file_token_event_proto_goTypes = nil + file_token_event_proto_depIdxs = nil +} diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go new file mode 100644 index 0000000..c687168 --- /dev/null +++ b/internal/kafka/spanproc/spanproc.go @@ -0,0 +1,187 @@ +package spanproc + +import ( + "context" + "fmt" + "log/slog" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/twmb/franz-go/pkg/kgo" + "google.golang.org/protobuf/proto" + + "github.com/ExplorViz/trace-service/internal/attrib" + "github.com/ExplorViz/trace-service/internal/conversion" + "github.com/ExplorViz/trace-service/internal/genproto/spanpb" + "github.com/ExplorViz/trace-service/internal/token" + + coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" +) + +var lastReceivedSpans atomic.Uint64 +var lastInvalidSpans atomic.Uint64 +var lastExportedSpans atomic.Uint64 +var lastDuplicateSpans atomic.Uint64 + +type spanCache struct { + mu sync.Mutex + m map[string]map[string]bool +} + +func newSpanCache() spanCache { + return spanCache{ + m: make(map[string]map[string]bool), + } +} + +func (s *spanCache) add(landscapeTokenID string, spanID string) { + s.mu.Lock() + defer s.mu.Unlock() + + m, ok := s.m[landscapeTokenID] + if !ok { + s.m[landscapeTokenID] = make(map[string]bool) + m = s.m[landscapeTokenID] + } + + m[spanID] = true +} + +func (s *spanCache) contains(landscapeTokenID string, spanID string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + m, ok := s.m[landscapeTokenID] + if !ok { + return false + } + + _, ok = m[spanID] + return ok +} + +func Run(ctx context.Context, cl *kgo.Client, tv token.TokenValidator, logInterval time.Duration) { + spans := make(chan *attrib.SpanReader) + results := make(chan *spanpb.ParsedSpan) + knownSpans := newSpanCache() + + workerCount := runtime.NumCPU() + var wg sync.WaitGroup + for range workerCount { + wg.Go(func() { consumerWorker(ctx, spans, results, &knownSpans, tv) }) + } + wg.Go(func() { producerWorker(ctx, results, cl) }) + + if logInterval > 0 { + go func() { + ticker := time.NewTicker(logInterval) + defer ticker.Stop() + + for range ticker.C { + slog.Info( + "logging span counts since last log interval", + "received", lastReceivedSpans.Swap(0), + "invalid", lastInvalidSpans.Swap(0), + "exported", lastExportedSpans.Swap(0), + "duplicates", lastDuplicateSpans.Swap(0)) + } + }() + } + + for { + fs := cl.PollFetches(ctx) + if ctx.Err() != nil { + slog.Debug("exiting kafka span poll loop") + break + } + fs.EachRecord(func(r *kgo.Record) { + var req coltracepb.ExportTraceServiceRequest + if err := proto.Unmarshal(r.Value, &req); err != nil { + slog.Debug("invalid protocol buffer for span", "error", err) + return + } + + for _, rs := range req.GetResourceSpans() { + for _, ss := range rs.GetScopeSpans() { + for _, s := range ss.GetSpans() { + sr := attrib.NewSpanReader(s, ss.GetScope(), rs.GetResource()) + spans <- &sr + } + } + } + }) + } + + wg.Wait() +} + +func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, results chan<- *spanpb.ParsedSpan, sc *spanCache, tv token.TokenValidator) { + for { + select { + case <-ctx.Done(): + slog.Debug("exiting span consumer worker") + return + + case sr := <-spans: + lastReceivedSpans.Add(1) + + if err := validate(sr, tv); err != nil { + lastInvalidSpans.Add(1) + slog.Debug("received invalid span", "error", err) + } + + tokenID := sr.TokenID() + if sc.contains(tokenID, sr.SpanID()) { + lastDuplicateSpans.Add(1) + slog.Debug("received already seen span ID", "spanID", sr.SpanID()) + continue + } + + p, err := conversion.ConvertSpan(sr) + if err != nil { + slog.Error("failed to convert span", "error", err) + continue + } + sc.add(tokenID, sr.SpanID()) + results <- conversion.ToProto(p) + } + } +} + +func producerWorker(ctx context.Context, results <-chan *spanpb.ParsedSpan, cl *kgo.Client) { + for { + select { + case <-ctx.Done(): + slog.Debug("exiting span producer worker") + return + + case s := <-results: + out, err := proto.Marshal(s) + if err != nil { + slog.Error("failed to encode protobuf", "error", err) + continue + } + cl.ProduceSync(ctx, &kgo.Record{ + Key: []byte(s.GetLandscapeTokenId()), + Value: out, + }) + lastExportedSpans.Add(1) + } + } +} + +func validate(sr *attrib.SpanReader, tv token.TokenValidator) error { + t := token.LandscapeToken{ID: sr.TokenID(), Secret: sr.TokenSecret()} + + if t.ID == "" || t.Secret == "" { + return fmt.Errorf("missing landscape token ID or secret attribute") + } + + if err := tv.Validate(t); err != nil { + return fmt.Errorf("invalid token: %v", err) + } + + return nil +} diff --git a/internal/kafka/tokenproc/tokenproc.go b/internal/kafka/tokenproc/tokenproc.go new file mode 100644 index 0000000..ed3f2b0 --- /dev/null +++ b/internal/kafka/tokenproc/tokenproc.go @@ -0,0 +1,50 @@ +// Package tokenproc is concerned with processing landscape token events, +// such as the creation of new tokens and the deletion of existing tokens. +package tokenproc + +import ( + "context" + "log/slog" + + "github.com/twmb/franz-go/pkg/kgo" + "google.golang.org/protobuf/proto" + + "github.com/ExplorViz/trace-service/internal/genproto/tokenpb" + "github.com/ExplorViz/trace-service/internal/token" +) + +// Run continuously fetches records from the given client, attempts to deserialize them +// as [tokenpb.TokenEvent]s and updates the provided [InMemTokenStore] accordingly. +func Run(ctx context.Context, cl *kgo.Client, ts *token.InMemTokenStore) { + for { + fs := cl.PollFetches(ctx) + if ctx.Err() != nil { + slog.Debug("exiting kafka token poll loop") + break + } + fs.EachRecord(func(r *kgo.Record) { + if r.Value == nil { + ts.Delete(string(r.Key)) + slog.Debug("received tombstone token record, deleting token", "tokenID", r.Key) + return + } + + var t tokenpb.TokenEvent + if err := proto.Unmarshal(r.Value, &t); err != nil { + slog.Debug("invalid protocol buffer for token event", "error", err) + return + } + + switch t.GetType() { + case tokenpb.EventType_EVENT_TYPE_CREATED: + ts.Put(token.LandscapeToken{ID: string(r.Key), Secret: t.GetToken().GetSecret()}) + slog.Debug("received token create event", "tokenID", t.GetToken().GetId(), "tokenSecret", t.GetToken().GetSecret()) + case tokenpb.EventType_EVENT_TYPE_DELETED: + ts.Delete(string(r.Key)) + slog.Debug("received token delete event", "tokenID", r.Key) + default: + slog.Warn("received unhandled token event type", "eventType", tokenpb.EventType_name[int32(t.GetType())]) + } + }) + } +} diff --git a/internal/parsing/code.go b/internal/parsing/code.go new file mode 100644 index 0000000..aaf868b --- /dev/null +++ b/internal/parsing/code.go @@ -0,0 +1,49 @@ +package parsing + +import ( + "cmp" + "errors" + "fmt" + + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" + + "github.com/ExplorViz/trace-service/internal/attrib" +) + +type CodeSpanEntity struct { + FilePath string + FuncName string + ClassName string + Language string + GitCommitHash string +} + +func (c CodeSpanEntity) Id() string { + return c.FilePath + " " + c.FuncName + " " + c.ClassName + " " + c.GitCommitHash +} + +func ParseCodeSpan(sr *attrib.SpanReader) (SpanEntity, error) { + fqn := sr.SpanAttribute(semconv.CodeFunctionNameKey).GetStringValue() + lang := sr.ResourceAttribute(semconv.TelemetrySDKLanguageKey).GetStringValue() + + parsedFqn := ParseFunctionFqn(fqn, lang) + + if parsedFqn.FuncName == "" { + return &CodeSpanEntity{}, errors.New("code parser: function name could not be extracted") + } + + filePath := cmp.Or(parsedFqn.FilePath, sr.SpanAttribute(semconv.CodeFilePathKey).GetStringValue()) + + if filePath == "" { + return CodeSpanEntity{}, fmt.Errorf("code parser: file path could not be extracted from FQN and %s not given", semconv.CodeFilePathKey) + } + + gitHash := sr.SpanAttribute(semconv.VCSRefHeadRevisionKey).GetStringValue() + + return CodeSpanEntity{ + FilePath: filePath, + FuncName: parsedFqn.FuncName, + ClassName: parsedFqn.ClassName, + Language: lang, + GitCommitHash: gitHash}, nil +} diff --git a/internal/parsing/common.go b/internal/parsing/common.go new file mode 100644 index 0000000..c584d6c --- /dev/null +++ b/internal/parsing/common.go @@ -0,0 +1,9 @@ +package parsing + +import "github.com/ExplorViz/trace-service/internal/attrib" + +type SpanParser func(sr *attrib.SpanReader) (SpanEntity, error) + +type SpanEntity interface { + Id() string +} diff --git a/internal/parsing/doc.go b/internal/parsing/doc.go new file mode 100644 index 0000000..72a81c6 --- /dev/null +++ b/internal/parsing/doc.go @@ -0,0 +1,8 @@ +// Package parsing is concerned with the interpretation of span attributes, +// with the goal of infering the entity within the system that the span describes. +// +// Various parsing functions and result types are defined which rely on different +// attributes from the [OTel semantic conventions]. +// +// [OTel semantic conventions]: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/ +package parsing diff --git a/internal/parsing/fqn.go b/internal/parsing/fqn.go new file mode 100644 index 0000000..e2cc243 --- /dev/null +++ b/internal/parsing/fqn.go @@ -0,0 +1,92 @@ +package parsing + +import ( + "log/slog" + "strings" + "unicode" +) + +type FqnParseResult struct { + FilePath string + FuncName string + ClassName string +} + +func ParseFunctionFqn(fqn string, language string) FqnParseResult { + switch strings.ToLower(language) { + case "java": + return parseJavaFqn(fqn) + default: + slog.Warn("unknown or missing SDK language, using generic fqn parser", "telemetry.sdk.language", language) + return parseGenericFqn(fqn) + } +} + +// ParseJavaFqn parses a Java method fully-qualified name (fqn) as specified by the [OTel semantic conventions]. +// Attempts to detect inner classes if multiple fqn segments are capitalized. +// +// Example fqn: +// +// "net.explorviz.app.MyConverter.MyInnerClass.convert" +// +// Example result: +// +// ["net/explorviz/app/MyConverter.java", "convert", "MyConverter.MyInnerClass"] +// +// [OTel semantic conventions]: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/ +func parseJavaFqn(fqn string) FqnParseResult { + const FileExtension = ".java" + + // Ignore lambda portion. If we explicitly want to support this, the data model would need to allow functions + // to have child functions. For now, we simplify and treat this as a call of the containing function. + fqnNoLambda, _, _ := strings.Cut(fqn, "$") + separatedFqn := strings.Split(fqnNoLambda, ".") + + lastPackageIndex := -1 + for i := len(separatedFqn) - 2; i >= 0; i-- { + if !startsWithUpper(separatedFqn[i]) { + lastPackageIndex = i + break + } + } + + fileNameIndex := lastPackageIndex + 1 + filePath := strings.Join(separatedFqn[:fileNameIndex+1], "/") + FileExtension + + className := "" + if fileNameIndex < len(separatedFqn)-1 { + className = strings.Join( + separatedFqn[fileNameIndex:len(separatedFqn)-1], + ".", + ) + } + + methodName := separatedFqn[len(separatedFqn)-1] + + return FqnParseResult{FilePath: filePath, FuncName: methodName, ClassName: className} +} + +// ParseGenericFqn acts as a fallback generic parser for fully-qualified function names where the language is not known. +// Assumes dot-separated string, where the last segment represents the function name and the prior segments represent the +// file's path. Does not add any file extension to the file path. Assumes no classes are included in the fqn. +// +// Example fqn: +// +// "src.main.SomeFile.myFunc" +// +// Example result: +// +// ["src/main/SomeFile", "myFunc", ""] +func parseGenericFqn(fqn string) FqnParseResult { + separatedFqn := strings.Split(fqn, ".") + return FqnParseResult{ + FilePath: strings.Join(separatedFqn[:len(separatedFqn)-1], "/"), + FuncName: separatedFqn[len(separatedFqn)-1]} +} + +func startsWithUpper(s string) bool { + for _, r := range s { + return unicode.IsUpper(r) + } + return false +} diff --git a/internal/token/validate.go b/internal/token/validate.go new file mode 100644 index 0000000..685c37e --- /dev/null +++ b/internal/token/validate.go @@ -0,0 +1,69 @@ +// Package token provides utilities for working with landscape tokens. +package token + +import ( + "fmt" + "sync" +) + +type LandscapeToken struct { + ID string + Secret string +} + +// A TokenValidator validates a landscape token, identified by its ID and secret. +type TokenValidator interface { + // Validate checks whether the given landscape token meets the requirements of this validator + // and returns an error if this is not the case. + Validate(token LandscapeToken) error +} + +// An InMemTokenStore uses a map to keep track of landscape tokens in-memory. +// Tokens can be manually added and removed from the store. +// The store is safe to read and write from multiple goroutines. +type InMemTokenStore struct { + mu sync.RWMutex + m map[string]string +} + +func NewInMemTokenStore() *InMemTokenStore { + return &InMemTokenStore{ + m: make(map[string]string), + } +} + +func (s *InMemTokenStore) Validate(t LandscapeToken) error { + s.mu.Lock() + defer s.mu.Unlock() + + val, ok := s.m[t.ID] + if ok && val == t.Secret { + return nil + } + return fmt.Errorf("unknown landscape token or incorrect secret") +} + +func (s *InMemTokenStore) Put(t LandscapeToken) { + s.mu.Lock() + defer s.mu.Unlock() + + s.m[t.ID] = t.Secret +} + +func (s *InMemTokenStore) Delete(tokenID string) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.m, tokenID) +} + +var _ TokenValidator = (*InMemTokenStore)(nil) + +// A NoOpTokenValidator stores no data and treats all landscape token as valid. +type NoOpTokenValidator struct{} + +func (s NoOpTokenValidator) Validate(t LandscapeToken) error { + return nil +} + +var _ TokenValidator = NoOpTokenValidator{} diff --git a/main.go b/main.go new file mode 100644 index 0000000..607108e --- /dev/null +++ b/main.go @@ -0,0 +1,118 @@ +//go:generate go run ./scripts/genproto/genproto.go +//go:generate go run ./scripts/reghook/reghook.go + +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/peterbourgon/ff/v4" + "github.com/peterbourgon/ff/v4/ffhelp" + "github.com/twmb/franz-go/pkg/kgo" + + "github.com/ExplorViz/trace-service/internal/kafka/spanproc" + "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" + "github.com/ExplorViz/trace-service/internal/token" +) + +func main() { + fs := ff.NewFlagSet("trace-service") + var ( + seedBroker = fs.String('b', "broker", "localhost:9091", "network endpoint of the Kafka broker to use") + inTopic = fs.String('i', "topic-in", "telemetry.spans.raw", "Kafka topic to consume OpenTelemetry OTLP spans from") + outTopic = fs.String('o', "topic-out", "telemetry.spans.parsed", "Kafka topic to produce parsed spans into") + tokensTopic = fs.String('t', "topic-tokens", "tokens.events", "Kafka topic to consume landscape token events from") + validateTokens = fs.Bool('v', "validate-tokens", "whether to verify the existence of provided landscape tokens for incoming traces") + logLevel = fs.StringEnum('l', "log-level", "log level: info, error, debug", "info", "error", "debug") + logInterval = fs.DurationLong("log-interval", 5*time.Second, "interval at which received spans should be logged (0 to disable)") + ) + if err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("EXPLORVIZ")); err != nil { + fmt.Println(err) + fmt.Printf("%s\n", ffhelp.Flags(fs)) + os.Exit(0) + } + + if *logInterval < 0 { + slog.Error("span logging duration must be positive") + flag.Usage() + os.Exit(1) + } + switch *logLevel { + case "info": + slog.SetLogLoggerLevel(slog.LevelInfo) + case "error": + slog.SetLogLoggerLevel(slog.LevelError) + case "debug": + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + spanCl, err := kgo.NewClient( + kgo.SeedBrokers(*seedBroker), + kgo.ConsumeTopics(*inTopic), + kgo.DefaultProduceTopic(*outTopic), + kgo.ConsumerGroup("trace-service"), + ) + if err != nil { + slog.Error("unable to initialize kgo client for spans", "error", err) + os.Exit(1) + } + defer spanCl.Close() + + tokenCl, err := kgo.NewClient( + kgo.SeedBrokers(*seedBroker), + kgo.ConsumeTopics(*tokensTopic), + kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()), // replay token events from beginning + ) + if err != nil { + slog.Error("unable to initialize kgo client for token events", "error", err) + os.Exit(1) + } + defer tokenCl.Close() + + ctx, cancel := context.WithCancel(context.Background()) + sigs := make(chan os.Signal, 2) + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigs + slog.Info("received interrupt signal; gracefully stopping ...") + cancel() + <-sigs + slog.Info("received second interrupt signal; exiting immediately") + os.Exit(1) + }() + + var wg sync.WaitGroup + + var tv token.TokenValidator + if *validateTokens { + inmem := token.NewInMemTokenStore() + wg.Go(func() { tokenproc.Run(ctx, tokenCl, inmem) }) + tv = inmem + } else { + tv = token.NoOpTokenValidator{} + } + wg.Go(func() { spanproc.Run(ctx, spanCl, tv, *logInterval) }) + + fmt.Print(` + ______ _ __ ___ + | ____| | | \ \ / (_) + | |__ __ ___ __ | | ___ _ _\ \ / / _ ____ + | __| \ \/ / '_ \| |/ _ \| '__\ \/ / | |_ / + | |____ > <| |_) | | (_) | | \ / | |/ / + |______/_/\_\ .__/|_|\___/|_| \/ |_/___| + | | + |_| trace-service + +`) + + <-ctx.Done() + wg.Wait() +} diff --git a/proto/parsed_span.proto b/proto/parsed_span.proto new file mode 100644 index 0000000..907ff19 --- /dev/null +++ b/proto/parsed_span.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +option go_package = "github.com/ExplorViz/trace-service/internal/genproto/spanpb"; + +message ParsedSpan { + string landscape_token_id = 1; + string landscape_token_secret = 2; + + string trace_id = 3; + string span_id = 4; + string span_name = 5; + optional string parent_id = 6; + + uint64 start_time = 7; + uint64 end_time = 8; + + string application_name = 9; + + string entity_id = 10; // Identifier for the entity described by the span (e.g. function, database, ...) + + oneof entity_descriptor { + CodeDescriptor code_descriptor = 11; + } +} + +message CodeDescriptor { + string file_path = 1; + string function_name = 2; + optional string class_name = 3; // Inner classes may be separated by "." + optional string language = 4; + optional string git_commit_hash = 5; +} \ No newline at end of file diff --git a/proto/token_event.proto b/proto/token_event.proto new file mode 100644 index 0000000..265034e --- /dev/null +++ b/proto/token_event.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +option go_package = "github.com/ExplorViz/trace-service/internal/genproto/tokenpb"; + +message TokenEvent { + EventType type = 1; + LandscapeToken token = 2; + optional string cloned_token_id = 3; +} + +enum EventType { + EVENT_TYPE_UNSPECIFIED = 0; + EVENT_TYPE_CREATED = 1; + EVENT_TYPE_DELETED = 2; + EVENT_TYPE_CLONED = 3; + EVENT_TYPE_ACCESS_GRANTED = 4; + EVENT_TYPE_ACCESS_REVOKED = 5; +} + +message LandscapeToken { + string id = 1; + string secret = 2; + string alias = 3; + string owner_id = 4; + uint64 created = 5; +} \ No newline at end of file diff --git a/scripts/genproto/genproto.go b/scripts/genproto/genproto.go new file mode 100644 index 0000000..ab34aca --- /dev/null +++ b/scripts/genproto/genproto.go @@ -0,0 +1,44 @@ +//go:build ignore + +// Command genproto generates the Go code required for this application from the .proto files +// within the proto directory of this repository. This is achieved by calling the protoc compiler, +// which must be installed for this script to work. +// +// The purpose of this dedicated script is to provide a cross-platform way to invoke the protoc command. +// As such, this command is not intended to be run directly and should instead be invoked using go:generate. +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +func main() { + fmt.Println("generating protobuf code") + + files, err := filepath.Glob("./proto/*.proto") + if err != nil { + fmt.Printf("error matching glob pattern: %v\n", err) + os.Exit(1) + } + + if len(files) == 0 { + fmt.Println("no matching proto files found") + return + } + + args := append([]string{"-I=./proto", "--go_out=.", "--go_opt=module=github.com/ExplorViz/trace-service"}, files...) + + cmd := exec.Command("protoc", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + fmt.Printf("protobuf generation failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("successfully generated protobuf code") +} diff --git a/scripts/reghook/reghook.go b/scripts/reghook/reghook.go new file mode 100644 index 0000000..6c9ff46 --- /dev/null +++ b/scripts/reghook/reghook.go @@ -0,0 +1,69 @@ +//go:build ignore + +// Command reghook registers the hooks provided within the githooks folder of this repository. +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func main() { + sourceDir := "githooks" + targetDir := filepath.Join(".git", "hooks") + + if err := os.MkdirAll(targetDir, 0755); err != nil { + fmt.Printf("failed to create hooks directory: %v\n", err) + os.Exit(1) + } + + entries, err := os.ReadDir(sourceDir) + if err != nil { + fmt.Printf("failed to read githooks directory: %v\n", err) + os.Exit(1) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + srcPath := filepath.Join(sourceDir, entry.Name()) + dstPath := filepath.Join(targetDir, entry.Name()) + + if err := copyFile(srcPath, dstPath); err != nil { + fmt.Printf("failed to copy %s: %v\n", entry.Name(), err) + continue + } + + if err := os.Chmod(dstPath, 0755); err != nil { + fmt.Printf("failed to set executable for %s: %v\n", entry.Name(), err) + } + + fmt.Println("installed hook:", entry.Name()) + } + + fmt.Println("successfully installed Git pre-commit hooks") +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return out.Sync() +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 13f1c07..0000000 --- a/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - mavenLocal() - } - plugins { - id "${quarkusPluginId}" version "${quarkusPluginVersion}" - } -} -rootProject.name = 'trace-service' diff --git a/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java b/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java deleted file mode 100644 index 7991018..0000000 --- a/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.NewTopic; -import org.testcontainers.containers.KafkaContainer; -import org.testcontainers.utility.DockerImageName; - -public class KafkaTestResource implements QuarkusTestResourceLifecycleManager { - - private static final KafkaContainer KAFKA = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.3.0")); - - @Override - public Map start() { - KAFKA.start(); - final Properties config = new Properties(); - config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA.getBootstrapServers()); - - final AdminClient localKafkaAdmin = AdminClient.create(config); - - final int partitions = 1; - final short replication = 1; - final NewTopic topicSpans = new NewTopic("explorviz-spans-dynamic", partitions, replication); - final NewTopic topicTokens = new NewTopic("token-events", partitions, replication); - final List topics = Arrays.asList(topicSpans, topicTokens); - - localKafkaAdmin.createTopics(topics); - - return Collections.singletonMap("kafka.bootstrap.servers", KAFKA.getBootstrapServers()); - } - - @Override - public void stop() { - KAFKA.stop(); - } -} diff --git a/src/main/avro/ExplorvizProtocol.avdl b/src/main/avro/ExplorvizProtocol.avdl deleted file mode 100644 index d4c7724..0000000 --- a/src/main/avro/ExplorvizProtocol.avdl +++ /dev/null @@ -1,49 +0,0 @@ -@namespace("net.explorviz.avro") -protocol ExplorvizProtocol { - enum EventType { - CREATED, - DELETED, - ACCESS_GRANTED, - ACCESS_REVOKED, - CLONED - } - - record LandscapeToken { - string value; - string ownerId; - string secret; - long created; - string alias; - boolean isRequestedFromVSCodeExtension = false; - string projectName = ""; - string commitId = ""; - } - - record TokenEvent { - EventType type; - LandscapeToken token; - string clonedToken; - } - - record Span { - string landscapeToken; - string gitCommitChecksum = "cross-commit"; - string spanId; - string parentSpanId; - string traceId; - long startTimeEpochNano; - long endTimeEpochNano; - string fullyQualifiedOperationName; - string hostname; - string hostIpAddress; - string appName; - string appInstanceId; - string appLanguage; - string hashCode = "default-hashcode"; - - string k8sPodName = ""; - string k8sNodeName = ""; - string k8sNamespace = ""; - string k8sDeploymentName = ""; - } -} diff --git a/src/main/avro/SpanData.avdl b/src/main/avro/SpanData.avdl deleted file mode 100644 index cb89c3b..0000000 --- a/src/main/avro/SpanData.avdl +++ /dev/null @@ -1,17 +0,0 @@ -@namespace("net.explorviz.persistence.avro") -protocol SpanDataProtocol { - record SpanData { - string spanId; - string parentId; - string traceId; - string landscapeTokenId; - long startTime; - long endTime; - string applicationName; - string filePath; - string functionName; - string language; - union { null, string } className = null; - union { null, string } commitHash = null; - } -} diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm deleted file mode 100644 index 79cb893..0000000 --- a/src/main/docker/Dockerfile.jvm +++ /dev/null @@ -1,97 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the container image run: -# -# ./gradlew build -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/explorviz.test-service-jvm . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-jvm -# -# If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. -# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 -# when running the container -# -# Then run the container using : -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-jvm -# -# This image uses the `run-java.sh` script to run the application. -# This scripts computes the command line to execute your Java application, and -# includes memory/GC tuning. -# You can configure the behavior using the following environment properties: -# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") -# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options -# in JAVA_OPTS (example: "-Dsome.property=foo") -# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is -# used to calculate a default maximal heap memory based on a containers restriction. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio -# of the container available memory as set here. The default is `50` which means 50% -# of the available memory is used as an upper boundary. You can skip this mechanism by -# setting this value to `0` in which case no `-Xmx` option is added. -# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This -# is used to calculate a default initial heap memory based on the maximum heap memory. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio -# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` -# is used as the initial heap size. You can skip this mechanism by setting this value -# to `0` in which case no `-Xms` option is added (example: "25") -# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. -# This is used to calculate the maximum value of the initial heap memory. If used in -# a container without any memory constraints for the container then this option has -# no effect. If there is a memory constraint then `-Xms` is limited to the value set -# here. The default is 4096MB which means the calculated value of `-Xms` never will -# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") -# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output -# when things are happening. This option, if set to true, will set -# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). -# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: -# true"). -# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). -# - CONTAINER_CORE_LIMIT: A calculated core limit as described in -# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") -# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). -# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. -# (example: "20") -# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. -# (example: "40") -# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. -# (example: "4") -# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus -# previous GC times. (example: "90") -# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") -# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") -# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should -# contain the necessary JRE command-line options to specify the required GC, which -# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). -# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") -# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") -# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be -# accessed directly. (example: "foo.example.com,bar.example.com") -# -### -FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 - -ENV LANGUAGE='en_US:en' - - -# We make four distinct layers so if there are application changes the library layers can be re-used -COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ -COPY --chown=185 build/quarkus-app/*.jar /deployments/ -COPY --chown=185 build/quarkus-app/app/ /deployments/app/ -COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ - -EXPOSE 8080 -USER 185 -ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" - -ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] - diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar deleted file mode 100644 index 4d42dae..0000000 --- a/src/main/docker/Dockerfile.legacy-jar +++ /dev/null @@ -1,93 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.package.jar.type=legacy-jar -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/explorviz.test-service-legacy-jar . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-legacy-jar -# -# If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. -# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 -# when running the container -# -# Then run the container using : -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-legacy-jar -# -# This image uses the `run-java.sh` script to run the application. -# This scripts computes the command line to execute your Java application, and -# includes memory/GC tuning. -# You can configure the behavior using the following environment properties: -# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") -# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options -# in JAVA_OPTS (example: "-Dsome.property=foo") -# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is -# used to calculate a default maximal heap memory based on a containers restriction. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio -# of the container available memory as set here. The default is `50` which means 50% -# of the available memory is used as an upper boundary. You can skip this mechanism by -# setting this value to `0` in which case no `-Xmx` option is added. -# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This -# is used to calculate a default initial heap memory based on the maximum heap memory. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio -# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` -# is used as the initial heap size. You can skip this mechanism by setting this value -# to `0` in which case no `-Xms` option is added (example: "25") -# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. -# This is used to calculate the maximum value of the initial heap memory. If used in -# a container without any memory constraints for the container then this option has -# no effect. If there is a memory constraint then `-Xms` is limited to the value set -# here. The default is 4096MB which means the calculated value of `-Xms` never will -# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") -# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output -# when things are happening. This option, if set to true, will set -# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). -# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: -# true"). -# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). -# - CONTAINER_CORE_LIMIT: A calculated core limit as described in -# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") -# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). -# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. -# (example: "20") -# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. -# (example: "40") -# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. -# (example: "4") -# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus -# previous GC times. (example: "90") -# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") -# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") -# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should -# contain the necessary JRE command-line options to specify the required GC, which -# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). -# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") -# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") -# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be -# accessed directly. (example: "foo.example.com,bar.example.com") -# -### -FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 - -ENV LANGUAGE='en_US:en' - - -COPY build/lib/* /deployments/lib/ -COPY build/*-runner.jar /deployments/quarkus-run.jar - -EXPOSE 8080 -USER 185 -ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" - -ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native deleted file mode 100644 index d83eaad..0000000 --- a/src/main/docker/Dockerfile.native +++ /dev/null @@ -1,27 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.native.enabled=true -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.native -t quarkus/explorviz.test-service . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service -# -### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root build/*-runner /work/application - -EXPOSE 8080 -USER 1001 - -ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro deleted file mode 100644 index e98a9cc..0000000 --- a/src/main/docker/Dockerfile.native-micro +++ /dev/null @@ -1,30 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. -# It uses a micro base image, tuned for Quarkus native executables. -# It reduces the size of the resulting container image. -# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.native.enabled=true -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/explorviz.test-service . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service -# -### -FROM quay.io/quarkus/quarkus-micro-image:2.0 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root build/*-runner /work/application - -EXPOSE 8080 -USER 1001 - -ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt b/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt deleted file mode 100644 index 711dd34..0000000 --- a/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt +++ /dev/null @@ -1,137 +0,0 @@ -package net.explorviz.trace.adapter.conversion - -import com.google.protobuf.InvalidProtocolBufferException -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest -import io.opentelemetry.proto.trace.v1.Span -import io.quarkus.scheduler.Scheduled -import jakarta.enterprise.context.ApplicationScoped -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import java.util.concurrent.atomic.AtomicInteger -import net.explorviz.trace.adapter.service.converter.SpanConverterImpl -import net.explorviz.avro.EventType -import net.explorviz.avro.TokenEvent -import net.explorviz.trace.adapter.service.validation.SpanValidator -import net.explorviz.trace.persistence.PersistenceSpan -import net.explorviz.trace.persistence.PersistenceSpanProcessor -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.common.utils.Bytes -import org.apache.kafka.streams.KeyValue -import org.apache.kafka.streams.StreamsBuilder -import org.apache.kafka.streams.Topology -import org.apache.kafka.streams.kstream.Consumed -import org.apache.kafka.streams.kstream.KStream -import org.apache.kafka.streams.kstream.Materialized -import org.apache.kafka.streams.kstream.Produced -import org.apache.kafka.streams.state.KeyValueStore -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.slf4j.Logger -import org.slf4j.LoggerFactory - - -/** Builds a KafkaStream topology instance with all its transformers. Entry point of the stream analysis. */ -@ApplicationScoped -class TopologyProducer { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(TopologyProducer::class.java) - } - - private val lastReceivedSpans = AtomicInteger(0) - private val lastInvalidSpans = AtomicInteger(0) - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in") lateinit var inTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.spans") lateinit var spansOutTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in.tokens") lateinit var tokensInTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.tokens-table") lateinit var tokensOutTopic: String - - @Inject lateinit var validator: SpanValidator - - @Inject lateinit var spanAvroSerde: SpecificAvroSerde - - @Inject lateinit var tokenEventAvroSerde: SpecificAvroSerde - - @Inject lateinit var spanConverter: SpanConverterImpl - - @Inject - lateinit var persistenceProcessor: PersistenceSpanProcessor - - - @Produces - fun buildTopology(): Topology { - val builder = StreamsBuilder() - - // BEGIN Conversion Stream - val spanByteStream: KStream = - builder.stream(inTopic, Consumed.with(Serdes.ByteArray(), Serdes.ByteArray())) - - val spanStream: KStream = - spanByteStream.flatMapValues { data -> - try { - val spanList = mutableListOf() - ExportTraceServiceRequest.parseFrom(data).resourceSpansList.forEach { resourceSpans -> - resourceSpans.scopeSpansList.forEach { scopeSpans -> spanList.addAll(scopeSpans.spansList) } - } - lastReceivedSpans.addAndGet(spanList.size) - spanList - } catch (e: InvalidProtocolBufferException) { - LOGGER.trace("Invalid protocol buffer: ${e.message}") - emptyList() - } - } - - // Validate Spans - val validSpanStream: KStream = - spanStream.flatMapValues { value -> - if (!validator.isValid(value)) { - lastInvalidSpans.incrementAndGet() - emptyList() - } else { - listOf(value) - } - } - - val persistenceStream: KStream = - validSpanStream.map { _, value -> - val span = spanConverter.fromOpenTelemetrySpan(value) - KeyValue(span.landscapeToken.toString(), span) - } - - persistenceStream.foreach { _ : String?, span: PersistenceSpan? -> - persistenceProcessor.accept( - span, - ) - } - // END Conversion Stream - - // BEGIN Token Stream - builder - .stream(tokensInTopic, Consumed.with(Serdes.String(), tokenEventAvroSerde)) - .filter { key, value -> - LOGGER.trace("Received token event for token value $key with event $value") - value == null || value.type == EventType.CREATED - } - .to(tokensOutTopic, Produced.with(Serdes.String(), tokenEventAvroSerde)) - - builder.globalTable( - tokensOutTopic, - Materialized.`as`>("token-events-global-store") - .withKeySerde(Serdes.String()) - .withValueSerde(tokenEventAvroSerde), - ) - // END Token Stream - - return builder.build() - } - - @Scheduled(every = "{explorviz.log.span.interval}") - fun logStatus() { - val spans = lastReceivedSpans.getAndSet(0) - val invalidSpans = lastInvalidSpans.getAndSet(0) - LOGGER.debug("Received $spans spans: ${spans - invalidSpans} valid, $invalidSpans invalid") - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt b/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt deleted file mode 100644 index 0080515..0000000 --- a/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.quarkus.arc.DefaultBean -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import net.explorviz.avro.TokenEvent -import org.apache.kafka.streams.KafkaStreams -import org.apache.kafka.streams.StoreQueryParameters -import org.apache.kafka.streams.errors.InvalidStateStoreException -import org.apache.kafka.streams.state.QueryableStoreTypes -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** Returns a {@link SchemaRegistryClient} that is used by this application with a maximum number of 10 schemas. */ -@Dependent -class ReadOnlyKeyValueStoreProducer { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(ReadOnlyKeyValueStoreProducer::class.java) - } - - @Inject lateinit var streams: KafkaStreams - - @Produces - @DefaultBean - fun getTokenEventStore(): ReadOnlyKeyValueStore { - while (true) { - try { - return streams.store( - StoreQueryParameters.fromNameAndType( - "token-events-global-store", - QueryableStoreTypes.keyValueStore(), - ), - ) - } catch (e: InvalidStateStoreException) { - LOGGER.debug("State store not yet initialized: ${e.message}") - LOGGER.debug("Will try again ...") - } - } - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt b/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt deleted file mode 100644 index 4f474d1..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.explorviz.trace.adapter.service - -import jakarta.enterprise.context.ApplicationScoped -import jakarta.inject.Inject -import net.explorviz.avro.TokenEvent -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore - -/** Service to access available landscape tokens, backed by GlobalKTable and state store access. */ -@ApplicationScoped -class TokenService @Inject constructor(private val keyValueStore: ReadOnlyKeyValueStore) { - - /** - * Checks whether a given token exists. - * - * @param tokenValue the token to check - * @return `true` if the given token value is in the list of valid tokens - */ - fun validLandscapeTokenValue(tokenValue: String): Boolean { - val potentialEvent = keyValueStore[tokenValue] - return potentialEvent?.token?.value == tokenValue - } - - /** - * Checks whether a given landscape token and secret exist and belong to each other. - * - * @param tokenValue the token to check - * @param tokenSecret the secret to check - * @return `true` if the given token value is valid and the secret belongs to this token. - */ - fun validLandscapeTokenValueAndSecret(tokenValue: String, tokenSecret: String): Boolean { - val potentialEvent = keyValueStore[tokenValue] - return potentialEvent?.token?.value == tokenValue && potentialEvent.token.secret == tokenSecret - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt deleted file mode 100644 index 96d171f..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt +++ /dev/null @@ -1,115 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.trace.v1.Span -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_APP_INSTANCE_ID -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_APP_LANG -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_APP_NAME -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_CLASS_FQN -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_FQN -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_GIT_COMMIT_CHECKSUM -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_HOST_IP -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_HOST_NAME -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_LANDSCAPE_SECRET -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_PACKAGE_NAME -import org.apache.commons.lang3.StringUtils - -/** Reads the attributes of a [Span]. If an attribute is not found under the standard name, fallback attribute names - * may be provided. The preferred attribute names should always conform to the [OpenTelemetry Semantic Conventions]( - * https://opentelemetry.io/docs/specs/semconv/) - */ -open class AttributesReader(private val span: Span) { - - companion object { - const val LANDSCAPE_TOKEN = "explorviz.token.id" - const val TOKEN_SECRET = "explorviz.token.secret" - const val VCS_REF_HEAD_REVISION = "vcs.ref.head.revision" - const val GIT_COMMIT_CHECKSUM = "git_commit_checksum" - const val HOST_NAME = "host" - const val HOST_IP = "host_address" - const val APPLICATION_NAME = "service.name" - const val APPLICATION_INSTANCE_ID = "service.instance.id" - const val APPLICATION_LANGUAGE = "telemetry.sdk.language" - const val CODE_FILE_PATH = "code.file.path" - const val CODE_FUNCTION_NAME = "code.function.name" - const val CODE_FUNCTION = "code.function" // Deprecated semconv code attribute - const val CODE_NAMESPACE = "code.namespace" // Deprecated semconv code attribute - const val METHOD_FQN = "java.fqn" - const val K8S_POD_NAME = "k8s.pod.name" - const val K8S_NAMESPACE_NAME = "k8s.namespace.name" - const val K8S_NODE_NAME = "k8s.node.name" - const val K8S_DEPLOYMENT_NAME = "k8s.deployment.name" - } - - private val attributes: Map = span.attributesList.associate { it.key to it.value } - - open fun getAsString(key: String): String? { - return attributes[key]?.stringValue - } - - val landscapeToken: String - get() = getAsString(LANDSCAPE_TOKEN) ?: DEFAULT_LANDSCAPE_TOKEN - - val secret: String - get() = getAsString(TOKEN_SECRET) ?: DEFAULT_LANDSCAPE_SECRET - - val hostName: String - get() = getAsString(HOST_NAME) ?: DEFAULT_HOST_NAME - - val hostIpAddress: String - get() = getAsString(HOST_IP) ?: DEFAULT_HOST_IP - - val gitCommitChecksum: String - get() = getAsString(VCS_REF_HEAD_REVISION) ?: getAsString(GIT_COMMIT_CHECKSUM) ?: DEFAULT_GIT_COMMIT_CHECKSUM - - val applicationName: String - get() = getAsString(APPLICATION_NAME) ?: DEFAULT_APP_NAME - - val applicationInstanceId: String - get() = getAsString(APPLICATION_INSTANCE_ID) ?: DEFAULT_APP_INSTANCE_ID - - val applicationLanguage: String - get() = getAsString(APPLICATION_LANGUAGE) ?: DEFAULT_APP_LANG - - val filePath: String - get() = getAsString(CODE_FILE_PATH) ?: "" - - val methodFqn: String - get() { - val codeFunctionName = getAsString(CODE_FUNCTION_NAME) - val codeNamespace = getAsString(CODE_NAMESPACE) - val codeFunction = getAsString(CODE_FUNCTION) - val methodFqn = getAsString(METHOD_FQN) - - return codeFunctionName - ?: codeNamespace?.let { namespace -> codeFunction?.let { function -> "$namespace.$function" } } - ?: methodFqn - ?: generateMethodFqnFromSpanName() - } - - open fun generateMethodFqnFromSpanName(): String { - val spanName = span.name - if (spanName.isNullOrEmpty()) return DEFAULT_FQN - - val hierarchyDepth = StringUtils.countMatches(spanName, ".") - - return when { - hierarchyDepth == 0 -> "$DEFAULT_CLASS_FQN.$spanName" - hierarchyDepth == 1 -> "$DEFAULT_PACKAGE_NAME.$spanName" - else -> spanName // Assume span name contains fully qualified name - } - } - - val k8sPodName: String - get() = getAsString(K8S_POD_NAME) ?: "" - - val k8sNamespace: String - get() = getAsString(K8S_NAMESPACE_NAME) ?: "" - - val k8sNodeName: String - get() = getAsString(K8S_NODE_NAME) ?: "" - - val k8sDeploymentName: String - get() = getAsString(K8S_DEPLOYMENT_NAME) ?: "" -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt deleted file mode 100644 index b375386..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -/** Contains constants for missing attribute values of spans. */ -object DefaultAttributeValues { - const val DEFAULT_LANDSCAPE_TOKEN = "mytokenvalue" - const val DEFAULT_LANDSCAPE_SECRET = "mytokensecret" - const val DEFAULT_GIT_COMMIT_CHECKSUM = "UNKNOWN" // NOPMD - const val DEFAULT_HOST_IP = "0.0.0.0" // NOPMD - const val DEFAULT_HOST_NAME = "UNKNOWN-HOST" - const val DEFAULT_APP_NAME = "UNKNOWN-APPLICATION" - const val DEFAULT_APP_INSTANCE_ID = "0" - const val DEFAULT_APP_LANG = "UNKNOWN" - const val DEFAULT_FILE_PATH = "" - const val DEFAULT_PACKAGE_NAME = "unknownpkg" - const val DEFAULT_CLASS_NAME = "UnknownClass" - - const val DEFAULT_CLASS_FQN = "$DEFAULT_PACKAGE_NAME.$DEFAULT_CLASS_NAME" - const val DEFAULT_METHOD = "unknownMethod" - const val DEFAULT_FQN = "$DEFAULT_CLASS_FQN.$DEFAULT_METHOD" -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt deleted file mode 100644 index 9f97a6f..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import com.google.common.io.BaseEncoding - -/** Helper class to convert IDs given as byte arrays to strings. */ -object IdHelper { - - private const val SPAN_ID_LEN = 8 - private const val TRACE_ID_LEN = 16 - - /** - * Converts a span ID represented as a byte array to a readable string encoded in base 16. - * - * @param spanId the ID - * @return base16 encoding of the ID - */ - fun convertSpanId(spanId: ByteArray): String { - return BaseEncoding.base16().lowerCase().encode(spanId, 0, SPAN_ID_LEN) - } - - /** - * Converts a trace ID represented as a byte array to a readable string encoded in base 16. - * - * @param traceId the ID - * @return base16 encoding of the ID - */ - fun convertTraceId(traceId: ByteArray): String { - return BaseEncoding.base16().lowerCase().encode(traceId, 0, TRACE_ID_LEN) - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt deleted file mode 100644 index d174a57..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import io.opentelemetry.proto.trace.v1.Span - -/** - * Converts OpenTelemetry spans into another type. - * - * @param the type spans are converted to. - */ -interface SpanConverter { - - /** Converts an OpenTelemetry {@link Span} into {@link T}. */ - fun fromOpenTelemetrySpan(ocSpan: Span): T -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt deleted file mode 100644 index f88ddff..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import jakarta.enterprise.context.ApplicationScoped -import java.util.* -import net.explorviz.trace.adapter.service.converter.fqn.DefaultParser -import net.explorviz.trace.adapter.service.converter.fqn.FqnParser -import net.explorviz.trace.adapter.service.converter.fqn.JavaFqnParser -import net.explorviz.trace.persistence.PersistenceSpan -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** Converts a [io.opentelemetry.proto.trace.v1.Span] to a [PersistenceSpan]. */ -@ApplicationScoped -class SpanConverterImpl : SpanConverter { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(SpanConverterImpl::class.java) - - private val JAVA_PARSER = JavaFqnParser() - private val DEFAULT_PARSER = DefaultParser() - - private val PARSERS = mapOf( - "java" to JAVA_PARSER, - ) - - fun getFqnParser(language: String): FqnParser { - return PARSERS[language.lowercase()] ?: DEFAULT_PARSER - } - } - - override fun fromOpenTelemetrySpan(ocSpan: io.opentelemetry.proto.trace.v1.Span): PersistenceSpan { - - val attributesReader = AttributesReader(ocSpan) - - val gitCommitChecksum = attributesReader.gitCommitChecksum - val spanId = IdHelper.convertSpanId(ocSpan.spanId.toByteArray()) - val traceId = IdHelper.convertTraceId(ocSpan.traceId.toByteArray()) - val startTime = ocSpan.startTimeUnixNano - val endTime = ocSpan.endTimeUnixNano - val nodeIpAddress = attributesReader.hostIpAddress - val hostName = attributesReader.hostName - val applicationName = attributesReader.applicationName - val applicationLanguage = attributesReader.applicationLanguage - val applicationInstance = attributesReader.applicationInstanceId - val filePathAttrib = attributesReader.filePath - val methodFqn = attributesReader.methodFqn - val k8sPodName = attributesReader.k8sPodName - val k8sNodeName = attributesReader.k8sPodName - val k8sNamespace = attributesReader.k8sNamespace - val k8sDeploymentName = attributesReader.k8sDeploymentName - - val landscapeToken = attributesReader.landscapeToken; - - val parentSpanId = if (ocSpan.parentSpanId.size() > 0) { - IdHelper.convertSpanId(ocSpan.parentSpanId.toByteArray()) - } else { - "" - } - - val parsingResult: FqnParser.ParsingResult = getFqnParser(applicationLanguage).parseFunctionFqn(methodFqn) - - // Explicit file path attribute takes precedence. If unset, then parse from fqn - val filePath = - if (filePathAttrib != DefaultAttributeValues.DEFAULT_FILE_PATH) filePathAttrib else parsingResult.filePath - - val functionName = parsingResult.functionName - val className = parsingResult.className - - return PersistenceSpan( - landscapeToken, - gitCommitChecksum, - spanId, - parentSpanId, - traceId, - startTime, - endTime, - nodeIpAddress, - hostName, - applicationName, - applicationLanguage ?: PersistenceSpan.DEFAULT_LANGUAGE, - applicationInstance, - filePath, - functionName, - className, - k8sPodName, - k8sNodeName, - k8sNamespace, - k8sDeploymentName, - ) - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt deleted file mode 100644 index 8151279..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.explorviz.trace.adapter.service.converter.fqn - -/** - * Fallback generic parser. Assumes dot-separated string, where the last segment represents the function name and - * the prior segments represent the file's fqn (e.g. "src/main/SomeFile/myFunc"). Does not add any file extension to - * the file fqn. Assumes no classes are included in the fqn. - */ -class DefaultParser : FqnParser { - - override fun parseFunctionFqn(functionFqn: String): FqnParser.ParsingResult { - val separatedFqn = functionFqn.split(".") - val fileFqn = separatedFqn.dropLast(1).joinToString(".") - val functionName = separatedFqn.last() - return FqnParser.ParsingResult(fileFqn, functionName); - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt deleted file mode 100644 index 79424a7..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.explorviz.trace.adapter.service.converter.fqn - -interface FqnParser { - - data class ParsingResult(val filePath: String, val functionName: String, val className: String? = null) - - /** - * Parses a function fqn according to some language specification and separates into: - * * the associated file's relative path as given from the fqn (separated by "/") The file path should crucially - * _include file extension_ if applicable. - * * the (unqualified) name of the function - * * the name of a class within the file, if included in the fqn, otherwise null. The name of any inner class is - * qualified against its parent classes (separated by "."). - * - * See [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/) for - * examples on how the fqn is structured for different languages. - */ - fun parseFunctionFqn(functionFqn: String): ParsingResult -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt b/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt deleted file mode 100644 index 94ebe2c..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.explorviz.trace.adapter.service.converter.fqn - -/** - * Parses Java method fqn as specified by - * [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/). - * - * Attempts to detect inner classes if multiple fqn segments are capitalized, e.g. - * `net.explorviz.app.MyClass.MyInnerClass.doSomething` would result in class name `MyClass.MyInnerClass`. - * - * Example fqn: `"net.explorviz.app.MyConverter.convert"` - * Example result: `["net/explorviz/app/MyConverter.java", "convert", "MyConverter"]` - */ -class JavaFqnParser : FqnParser { - companion object { - const val FILE_EXTENSION = ".java" - } - - override fun parseFunctionFqn(functionFqn: String): FqnParser.ParsingResult { - // Ignore lambda portion. If we explicitly want to support this, the data model would need to allow functions to - // have child functions. For now, we simplify and treat this as a call of the containing function. - val fqnWithoutLambda = functionFqn.substringBefore("$") - val separatedFqn = fqnWithoutLambda.split(".") - - val lastPackageIndex = separatedFqn.dropLast(1).indexOfLast { it.firstOrNull()?.isUpperCase() != true } - val fileNameIndex = lastPackageIndex + 1 - - val filePath = separatedFqn.take(fileNameIndex + 1).joinToString("/") + FILE_EXTENSION - val className = separatedFqn.subList(fileNameIndex, separatedFqn.lastIndex).joinToString(".") - val methodName = separatedFqn.last() - - return FqnParser.ParsingResult(filePath, methodName, className) - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt b/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt deleted file mode 100644 index 1a3f9c2..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt +++ /dev/null @@ -1,155 +0,0 @@ -package net.explorviz.trace.adapter.span.service.validation - -import io.opentelemetry.proto.trace.v1.Span -import jakarta.enterprise.context.ApplicationScoped -import jakarta.inject.Inject -import java.nio.file.Paths -import java.time.DateTimeException -import net.explorviz.trace.adapter.service.converter.AttributesReader -import net.explorviz.trace.adapter.service.TokenService -import net.explorviz.trace.adapter.service.validation.SpanValidator -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -@ApplicationScoped -class DefaultSpanValidator -@Inject constructor( - private val tokenService: TokenService, - @ConfigProperty(name = "explorviz.validate.token-existence") var validateTokens: Boolean = false -) : SpanValidator { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(DefaultSpanValidator::class.java) - private const val MIN_DEPTH_FQN_NAME = 2 // Method FQN must at least include class / file if no file path is set - } - - override fun isValid(span: Span): Boolean { - val attr = AttributesReader(span) - return validateTimestamp(span.startTimeUnixNano) && validateTimestamp(span.endTimeUnixNano) && isValid(attr) - } - - fun isValid(spanAttributes: AttributesReader): Boolean { - return validateToken( - spanAttributes.landscapeToken, - spanAttributes.secret, - ) && validateHost( - spanAttributes.hostName, - spanAttributes.hostIpAddress, - ) && validateApp(spanAttributes.applicationName, spanAttributes.applicationLanguage) && validateOperation( - spanAttributes.methodFqn, spanAttributes.filePath, - ) && validateK8s(spanAttributes) - } - - private fun validateToken(token: String?, givenSecret: String?): Boolean { - val hasTokenAndSecret = !token.isNullOrBlank() && !givenSecret.isNullOrBlank() - - if (token.isNullOrBlank()) { - LOGGER.trace("Invalid span: No or blank token.") - } - - if (givenSecret.isNullOrBlank()) { - LOGGER.trace("Invalid span: No or blank secret.") - } - - var isValid = true - if (hasTokenAndSecret && validateTokens) { - isValid = tokenService.validLandscapeTokenValueAndSecret(token!!, givenSecret!!) - if (!isValid) { - LOGGER.trace("Invalid span: Token and/or secret are unknown.") - } - } - - return hasTokenAndSecret && isValid - } - - private fun validateTimestamp(timestamp: Long): Boolean { - return try { - if (timestamp <= 0L) { - throw NumberFormatException("Time must be positive") - } - true - } catch (e: DateTimeException) { - LOGGER.trace("Invalid span timestamp: Date time exception - ${e.message}") - false - } catch (e: NumberFormatException) { - LOGGER.trace("Invalid span timestamp: Number format exception - ${e.message}") - false - } - } - - private fun validateHost(hostName: String?, hostIp: String?): Boolean { - val isValid = !hostName.isNullOrBlank() && !hostIp.isNullOrBlank() - - if (hostName.isNullOrBlank()) { - LOGGER.trace("Invalid span: No hostname.") - } - - if (hostIp.isNullOrBlank()) { - LOGGER.trace("Invalid span: No IP address.") - } - - return isValid - } - - private fun validateApp(appName: String?, appLang: String?): Boolean { - val isValid = !appName.isNullOrBlank() && !appLang.isNullOrBlank() - - if (appName.isNullOrBlank()) { - LOGGER.trace("Invalid span: No application name.") - } - - if (appLang.isNullOrBlank()) { - LOGGER.trace("Invalid span: No application language given.") - } - - return isValid - } - - private fun validateOperation(fqn: String, filePath: String): Boolean { - // Despite OTel semconv, absolute file paths are not desirable as we don't know where the application begins - val filePathIsValid = - filePath.isNotBlank() && runCatching { Paths.get(filePath).isAbsolute }.getOrDefault(false) - - val operationFqnSplit = fqn.split(".") - - if (filePathIsValid) { - if (operationFqnSplit.last().isBlank()) { - LOGGER.trace("Invalid span: Invalid operation name {}", fqn) - return false - } - return true - } else { - if (operationFqnSplit.size < MIN_DEPTH_FQN_NAME) { - LOGGER.trace( - "Invalid span: Operation name \"{}\" too short with invalid file path given: \"{}\"", - fqn, - filePath, - ) - return false - } - if (operationFqnSplit.any { it.isBlank() }) { - LOGGER.trace( - "Invalid span: Operation name \"{}\" contains empty segments with invalid file path given: \"{}\"", - fqn, - filePath, - ) - return false - } - } - - return true - } - - private fun validateK8s(spanAttributes: AttributesReader): Boolean { - val hasPodName = spanAttributes.k8sPodName.isNotEmpty() - val hasNamespace = spanAttributes.k8sNamespace.isNotEmpty() - val hasNodeName = spanAttributes.k8sNodeName.isNotEmpty() - val hasDeployment = spanAttributes.k8sDeploymentName.isNotEmpty() - - val hasAll = hasPodName && hasNamespace && hasNodeName && hasDeployment - val hasNone = !hasPodName && !hasNamespace && !hasNodeName && !hasDeployment - - return hasAll || hasNone - } -} diff --git a/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt b/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt deleted file mode 100644 index 81b9dd6..0000000 --- a/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.explorviz.trace.adapter.service.validation - -import io.opentelemetry.proto.trace.v1.Span - -/** Validates [Span]s prior to processing. */ -interface SpanValidator { - - /** - * Checks if the given span is valid. - * - * @param span the span - * @return Boolean that indicates if span is valid. - */ - fun isValid(span: Span): Boolean -} diff --git a/src/main/java/net/explorviz/trace/application/StartupLogger.java b/src/main/java/net/explorviz/trace/application/StartupLogger.java deleted file mode 100644 index cfb180e..0000000 --- a/src/main/java/net/explorviz/trace/application/StartupLogger.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.explorviz.trace.application; - -import io.quarkus.runtime.StartupEvent; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@ApplicationScoped -public class StartupLogger { - - private static final Logger LOGGER = LoggerFactory.getLogger(StartupLogger.class); - - @ConfigProperty(name = "explorviz.span.api.timeverification.enabled") - /* default */ boolean isTimeVerificationEnabled; - - void onStart(@Observes final StartupEvent ev) { - LOGGER.atInfo().log("The application is starting..."); - - LOGGER.atInfo().addArgument(isTimeVerificationEnabled).log("API time ranges are enabled: {}"); - } - -} diff --git a/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java b/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java deleted file mode 100644 index c933127..0000000 --- a/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.quarkus.runtime.annotations.RegisterForReflection; -import java.util.Map; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.errors.RecordTooLargeException; -import org.apache.kafka.streams.errors.ErrorHandlerContext; -import org.apache.kafka.streams.errors.ProductionExceptionHandler; - -/** - * {@link ProductionExceptionHandler} used in the application.properties file. - * ({@link RecordTooLargeException}) lead to loss of records, but are not an unrecoverable error - * that should shut down the application. Therefore, we catch the exception, discard the record, and - * proceed. - */ -@RegisterForReflection -public class IgnoreRecordTooLargeHandler implements ProductionExceptionHandler { - - @Override - public void configure(final Map configs) { - // Nothing to do - } - - @Override - public ProductionExceptionHandlerResponse handle(final ErrorHandlerContext context, - final ProducerRecord record, final Exception exception) { - if (exception instanceof RecordTooLargeException) { - return ProductionExceptionHandlerResponse.CONTINUE; - } else { - return ProductionExceptionHandlerResponse.FAIL; - } - } -} diff --git a/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java b/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java deleted file mode 100644 index f0b585a..0000000 --- a/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.quarkus.arc.DefaultBean; -import jakarta.enterprise.inject.Produces; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -/** - * Returns a {@link SchemaRegistryClient} that is used by this application with a maximum number of - * 10 schemas. - */ -public class SchemaRegistryClientProducer { - - private static final int MAX_NUM_OF_SCHEMAS = 10; - - @ConfigProperty(name = "quarkus.kafka-streams.schema-registry-url") - /* default */ String schemaRegistryUrl; - - @Produces - @DefaultBean - public SchemaRegistryClient produceSchemaRegistryClient() { - return new CachedSchemaRegistryClient(this.schemaRegistryUrl, MAX_NUM_OF_SCHEMAS); - } -} diff --git a/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java b/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java deleted file mode 100644 index 3b643eb..0000000 --- a/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; -import io.quarkus.arc.DefaultBean; -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; -import java.util.Map; -import org.apache.avro.specific.SpecificRecord; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -/** - * Returns an injectable {@link SpecificAvroSerde}. - */ -@Dependent -public class SerdeProducer { - - @Inject - /* default */ SchemaRegistryClient registry; - - @ConfigProperty(name = "quarkus.kafka-streams.schema-registry-url") - /* default */ String schemaRegistryUrl; - - @Produces - @DefaultBean - public SpecificAvroSerde produceSpecificAvroSerde() { - final SpecificAvroSerde valueSerde = new SpecificAvroSerde<>(this.registry); - valueSerde.configure( - Map.of(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, this.schemaRegistryUrl), - false); - return valueSerde; - } -} diff --git a/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java b/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java deleted file mode 100644 index 0b91840..0000000 --- a/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.quarkus.runtime.Quarkus; -import io.quarkus.runtime.ShutdownEvent; -import io.quarkus.runtime.StartupEvent; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.KafkaStreams.State; -import org.apache.kafka.streams.KafkaStreams.StateListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Registers a {@link StateListener} for the {@link KafkaStreams} object that is used as - * ErrorStateListener. The Quarkus application does not shut down automatically if the Kafka Streams - * thread goes into {@link State}.ERROR state. The Kafka streams ERROR state has no come back, - * therefore the application is dead. - */ -@ApplicationScoped -public class ShutdownHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(ShutdownHandler.class); - - @Inject - /* default */ KafkaStreams streams; - - /* default */ void onStart(@Observes final StartupEvent ev) { - this.streams.setStateListener(new ErrorStateListener()); - } - - /* default */ void onStop(@Observes final ShutdownEvent ev) { - // nothing to do - } - - private static class ErrorStateListener implements StateListener { - - @Override - public void onChange(final State newState, final State oldState) { - if (newState == State.ERROR) { - - LOGGER.atError().log("Kafka Streams thread died. Are Kafka topic initialized? " - + "Quarkus application will shut down."); - - LOGGER.atError().log("About to system exit due to Kafka Streams Error."); - - Quarkus.asyncExit(-1); - Quarkus.waitForExit(); - System.exit(-1); // NOPMD - } - - } - } -} diff --git a/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java b/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java deleted file mode 100644 index 379e7bf..0000000 --- a/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.explorviz.trace.messaging; - -import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import net.explorviz.persistence.avro.SpanData; -import net.explorviz.trace.persistence.PersistenceSpan; -import org.eclipse.microprofile.reactive.messaging.Channel; -import org.eclipse.microprofile.reactive.messaging.Emitter; -import org.eclipse.microprofile.reactive.messaging.Message; - -@ApplicationScoped -public class KafkaSpanExporter { - - @Inject - @Channel("explorviz-spans") - Emitter emitter; - - public void persistSpan(PersistenceSpan span) { - SpanData spanData = mapToAvro(span); - - emitter.send( - Message.of(spanData) - .addMetadata( - OutgoingKafkaRecordMetadata.builder().withKey(span.traceId()).build())); - } - - private SpanData mapToAvro(PersistenceSpan span) { - return SpanData.newBuilder() - .setSpanId(span.spanId()) - .setParentId(span.parentSpanId()) - .setTraceId(span.traceId()) - .setLandscapeTokenId(span.landscapeToken().toString()) - .setStartTime(span.startTime()) - .setEndTime(span.endTime()) - .setApplicationName(span.applicationName()) - .setLanguage(span.applicationLanguage()) - .setFunctionName(span.functionName()) - .setFilePath(span.filePath()) - .setClassName( - span.className().isBlank() ? null : span.className()) - .setCommitHash( - span.gitCommitChecksum().isBlank() - ? null - : span.gitCommitChecksum()) - .build(); - } -} diff --git a/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java b/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java deleted file mode 100644 index aeb4ffa..0000000 --- a/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.explorviz.trace.persistence; - -public record PersistenceSpan( - String landscapeToken, - String gitCommitChecksum, - String spanId, - String parentSpanId, - String traceId, - long startTime, - long endTime, - String nodeIpAddress, // TODO: Convert into InetAddress type? - String hostName, - String applicationName, - String applicationLanguage, - String applicationInstance, - String filePath, - String functionName, - String className, - String k8sPodName, - String k8sNodeName, - String k8sNamespace, - String k8sDeploymentName -) { - public static final String DEFAULT_LANGUAGE = "LANGUAGE_UNSPECIFIED"; -} diff --git a/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java b/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java deleted file mode 100644 index 5fc16f4..0000000 --- a/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.explorviz.trace.persistence; - -import io.quarkus.scheduler.Scheduled; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import net.explorviz.trace.messaging.KafkaSpanExporter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@ApplicationScoped -public class PersistenceSpanProcessor implements Consumer { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersistenceSpanProcessor.class); - - private final AtomicLong lastProcessedSpans = new AtomicLong(0L); - private final AtomicLong lastExportedSpans = new AtomicLong(0L); - private final AtomicLong lastFailures = new AtomicLong(0L); - - private final ConcurrentMap> knownSpanIdsByLandscape = - new ConcurrentHashMap<>(1); - - @Inject - /* default */ KafkaSpanExporter exporter; - - @Override - public void accept(final PersistenceSpan span) { - final Set knownSpanIds = knownSpanIdsByLandscape.computeIfAbsent(span.landscapeToken(), - tokenValue -> ConcurrentHashMap.newKeySet()); - if (knownSpanIds.add(span.spanId())) { - exporter.persistSpan(span); - lastExportedSpans.incrementAndGet(); - } - - lastProcessedSpans.incrementAndGet(); - } - - @Scheduled(every = "{explorviz.log.span.interval}") - public void logStatus() { - final long processedSpans = this.lastProcessedSpans.getAndSet(0); - final long exportedSpans = this.lastExportedSpans.getAndSet(0); - LOGGER.atDebug().addArgument(processedSpans).addArgument(exportedSpans) - .log("Processed {} spans, exported {} spans."); - final long failures = this.lastFailures.getAndSet(0); - if (failures != 0) { - LOGGER.atWarn().addArgument(failures).log("Data loss occurred. {} span exports failed."); - } - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index ab20b48..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,78 +0,0 @@ -quarkus.banner.path=explorviz-banner.txt -quarkus.devservices.enabled=false -cron.expr=*/10 * * * * ? - -########## -## HTTP ## -########## -%dev.quarkus.http.port=8083 -%dev.quarkus.http.host=0.0.0.0 -%dev.quarkus.http.cors=true -%dev.quarkus.http.cors.origins=/.*/ - -########### -## Kafka ## -########### -# necessary for mp.messaging -kafka.bootstrap.servers=kafka:9092 -%dev.kafka.bootstrap.servers=localhost:9091 -%test.kafka.bootstrap.servers=localhost:9091 - -################### -## Kafka Streams ## -################### -quarkus.kafka-streams.bootstrap-servers=${kafka.bootstrap.servers} -quarkus.kafka-streams.application-id=trace-service -quarkus.kafka-streams.topics=explorviz-spans,token-events -quarkus.kafka-streams.schema-registry-url=http://schema-registry:8081 -%dev.quarkus.kafka-streams.schema-registry-url=http://localhost:8081 - -# Pass through options (directly) to Kafka Streams Config -kafka-streams.commit.interval.ms=5000 -%test.kafka-streams.state.dir=target/data/kafka-data/stores -kafka-streams.default.production.exception.handler=net.explorviz.trace.kafka.IgnoreRecordTooLargeHandler - -# Pass through options (directly) to Kafka Consumer and Producer Configs -#kafka-streams.consumer.fetch.max.wait.ms=500 -kafka-streams.producer.max.request.size=10485760 -kafka-streams.consumer.max.partition.fetch.bytes=10485760 -kafka-streams.consumer.fetch.max.bytes=57671680 - -mp.messaging.outgoing.explorviz-spans.connector=smallrye-kafka -mp.messaging.outgoing.explorviz-spans.topic=explorviz-spans -mp.messaging.outgoing.explorviz-spans.value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer - -mp.messaging.connector.smallrye-kafka.schema.registry.url=http://schema-registry:8081 -%dev.mp.messaging.connector.smallrye-kafka.schema.registry.url=http://localhost:8081 - -# Custom options -explorviz.kafka-streams.topics.in=otlp_spans -explorviz.span.api.timeverification.enabled=${EXPLORVIZ_SPAN_API_TIME_RANGES_ENABLED:true} -explorviz.kafka-streams.topics.in.tokens=token-events -explorviz.kafka-streams.topics.out.spans=explorviz-spans -explorviz.kafka-streams.topics.out.tokens-table=token-events-table - - -explorviz.validate.token-existence=true -%dev.explorviz.validate.token-existence=true -%test.explorviz.validate.token-existence=false - -# discard all incoming records for performance testing -############################# -## ExplorViz Logging Level ## -############################# -quarkus.log.level=INFO -quarkus.log.min-level=TRACE -quarkus.log.category."net.explorviz".level=${EXPLORVIZ_LOG_LVL:INFO} -%dev.quarkus.log.category."net.explorviz".level=${EXPLORVIZ_LOG_LVL:DEBUG} -%test.quarkus.log.category."net.explorviz".level=${EXPLORVIZ_LOG_LVL:TRACE} - -explorviz.log.span.interval=${EXPLORVIZ_LOG_SPAN_INTERVAL:1s} - -##################### -## Jib Image Build ## -##################### -quarkus.container-image.group=explorviz -quarkus.container-image.name=trace-service -quarkus.container-image.tag=latest -quarkus.jib.base-jvm-image=eclipse-temurin:21.0.1_12-jdk-alpine@sha256:075f8207cbfecf7e509e2d93e2ffc36c097b52a8109a9bfd40326fa12bd5c438 diff --git a/src/main/resources/explorviz-banner.txt b/src/main/resources/explorviz-banner.txt deleted file mode 100644 index 2724f1a..0000000 --- a/src/main/resources/explorviz-banner.txt +++ /dev/null @@ -1,11 +0,0 @@ - - - ______ _ __ ___ - | ____| | | \ \ / (_) - | |__ __ ___ __ | | ___ _ _\ \ / / _ ____ - | __| \ \/ / '_ \| |/ _ \| '__\ \/ / | |_ / - | |____ > <| |_) | | (_) | | \ / | |/ / - |______/_/\_\ .__/|_|\___/|_| \/ |_/___| - | | - |_| - diff --git a/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt b/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt deleted file mode 100644 index 18902cd..0000000 --- a/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt +++ /dev/null @@ -1,350 +0,0 @@ -package net.explorviz.trace.adapter.conversion - -import com.google.common.io.BaseEncoding -import com.google.protobuf.ByteString -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.common.v1.KeyValue -import io.opentelemetry.proto.trace.v1.ResourceSpans -import io.opentelemetry.proto.trace.v1.ScopeSpans -import io.opentelemetry.proto.trace.v1.Span -import io.quarkus.test.junit.QuarkusTest -import jakarta.inject.Inject -import java.nio.charset.Charset -import java.util.* -import net.explorviz.trace.adapter.service.TokenService -import net.explorviz.trace.adapter.service.converter.AttributesReader -import net.explorviz.trace.adapter.service.converter.IdHelper -import net.explorviz.avro.EventType -import net.explorviz.avro.LandscapeToken -import net.explorviz.avro.TokenEvent -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.streams.StreamsConfig -import org.apache.kafka.streams.TestInputTopic -import org.apache.kafka.streams.TestOutputTopic -import org.apache.kafka.streams.Topology -import org.apache.kafka.streams.TopologyTestDriver -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -@QuarkusTest -class TopologyTest { - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in") lateinit var inTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.spans") lateinit var spanOutTopicKey: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in.tokens") lateinit var tokensInTopic: String - - @Inject lateinit var topology: Topology - - @Inject lateinit var spanSerDe: SpecificAvroSerde - - @Inject lateinit var tokenEventSerDe: SpecificAvroSerde - - private var driver: TopologyTestDriver? = null - private var inputTopic: TestInputTopic? = null - private var inputTopicTokenEvents: TestInputTopic? = null - private var spanOutputTopic: TestOutputTopic? = null - private var tokenEventStore: ReadOnlyKeyValueStore? = null - - @BeforeEach - fun setUp() { - val config = - Properties().apply { - put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String()::class.java.name) - put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, SpecificAvroSerde::class.java.name) - put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, "http://registry:1234") - } - - driver = TopologyTestDriver(topology, config) - - inputTopic = - driver!!.createInputTopic(inTopic, Serdes.ByteArray().serializer(), Serdes.ByteArray().serializer()) - inputTopicTokenEvents = - driver!!.createInputTopic(tokensInTopic, Serdes.String().serializer(), tokenEventSerDe.serializer()) - spanOutputTopic = - driver!!.createOutputTopic(spanOutTopicKey, Serdes.String().deserializer(), spanSerDe.deserializer()) - - tokenEventStore = driver!!.getKeyValueStore("token-events-global-store") - } - - @AfterEach - fun tearDown() { - spanSerDe.close() - driver?.close() - } - - private fun sampleSpan(): Span { - val attributes = - listOf( - KeyValue.newBuilder() - .setKey(AttributesReader.LANDSCAPE_TOKEN) - .setValue(AnyValue.newBuilder().setStringValue("token").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.GIT_COMMIT_CHECKSUM) - .setValue(AnyValue.newBuilder().setStringValue("testGitCommit").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.TOKEN_SECRET) - .setValue(AnyValue.newBuilder().setStringValue("secret").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.HOST_NAME) - .setValue(AnyValue.newBuilder().setStringValue("hostname").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.HOST_IP) - .setValue(AnyValue.newBuilder().setStringValue("1.2.3.4").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.APPLICATION_NAME) - .setValue(AnyValue.newBuilder().setStringValue("appname").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.APPLICATION_INSTANCE_ID) - .setValue(AnyValue.newBuilder().setStringValue("1234").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.APPLICATION_LANGUAGE) - .setValue(AnyValue.newBuilder().setStringValue("language").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.METHOD_FQN) - .setValue(AnyValue.newBuilder().setStringValue("net.example.Bar.foo()").build()) - .build(), - ) - - return Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(1668069002431000000L) - .setEndTimeUnixNano(1668072086000000000L) - .addAllAttributes(attributes) - .build() - } - - private fun generateContainerForSpan(span: Span): ExportTraceServiceRequest { - val container2 = ScopeSpans.newBuilder().addSpans(span).build() - val container1 = ResourceSpans.newBuilder().addScopeSpans(container2).build() - return ExportTraceServiceRequest.newBuilder().addResourceSpans(container1).build() - } - - // Tests cause errors since conversion of spans now also triggers persistence of spans - -// @Test -// fun testAttributeTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// val result = spanOutputTopic!!.readKeyValue().value -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// assertEquals(attrs[AttributesReader.LANDSCAPE_TOKEN], result.landscapeToken, "Invalid token") -// assertEquals( -// attrs[AttributesReader.GIT_COMMIT_CHECKSUM], -// result.gitCommitChecksum, -// "Invalid Git Commit Checksum", -// ) -// assertEquals(attrs[AttributesReader.HOST_IP], result.hostIpAddress, "Invalid host ip address") -// assertEquals(attrs[AttributesReader.HOST_NAME], result.hostname, "Invalid host name") -// assertEquals(attrs[AttributesReader.APPLICATION_NAME], result.appName, "Invalid application name") -// assertEquals(attrs[AttributesReader.APPLICATION_INSTANCE_ID], result.appInstanceId, "Invalid application pid") -// assertEquals(attrs[AttributesReader.APPLICATION_LANGUAGE], result.appLanguage, "Invalid application language") -// assertEquals(attrs[AttributesReader.METHOD_FQN], result.fullyQualifiedOperationName, "Invalid operation name") -// } -// -// @Test -// fun testIdTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// -// assertFalse(spanOutputTopic!!.isEmpty, "Output topic is empty, but should contain a data record") -// -// val result = spanOutputTopic!!.readValue() -// -// val sid = BaseEncoding.base16().encode(testSpan.spanId.toByteArray(), 0, 8) -// assertEquals(sid, result.spanId) -// } -// -// @Test -// fun testTimestampTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// val result = spanOutputTopic!!.readKeyValue().value -// -// val expectedTimestamp = sampleSpan().startTimeUnixNano / 1_000_000L -// assertEquals(expectedTimestamp, result.startTimeEpochNano) -// } -// -// @Test -// fun testDynamicTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// val result = spanOutputTopic!!.readValue() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedToken = attrs[AttributesReader.LANDSCAPE_TOKEN] -// val expectedSpanId = IdHelper.convertSpanId(testSpan.spanId.toByteArray()) -// val expectedParentSpanId = IdHelper.convertSpanId(testSpan.parentSpanId.toByteArray()) -// val expectedStartTimeInMillisec = testSpan.startTimeUnixNano / 1_000_000L -// val expectedEndTimeInMillisec = testSpan.endTimeUnixNano / 1_000_000L -// -// assertEquals(expectedToken, result.landscapeToken, "Invalid token") -// assertEquals(expectedSpanId, result.spanId, "Invalid span ID") -// assertEquals(expectedParentSpanId, result.parentSpanId, "Invalid parent span ID") -// assertEquals(expectedStartTimeInMillisec, result.startTimeEpochNano, "Invalid start time") -// assertEquals(expectedEndTimeInMillisec, result.endTimeEpochNano, "Invalid end time") -// } -// -// @Test -// fun testTokenEventCreateInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(EventType.CREATED).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// -// val resultFromStateStore = tokenEventStore!!.get(expectedTokenValue) -// assertEquals(resultFromStateStore, expectedTokenEvent, "Invalid token event in state store") -// } -// -// @Test -// fun testTokenEventDeleteInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(EventType.CREATED).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// -// val tokenService = TokenService(tokenEventStore!!) -// -// val resultFromStateStore = tokenService.validLandscapeTokenValue(expectedTokenValue) -// assertTrue(resultFromStateStore, "Invalid token event in state store") -// -// // Now delete the event -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, null) -// -// val resultFromStateStore2 = tokenService.validLandscapeTokenValue(expectedTokenValue) -// assertFalse(resultFromStateStore2, "Invalid token event in state store, should be null") -// } -// -// @Test -// fun testTokenEventInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(EventType.CREATED).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// -// val tokenService = TokenService(tokenEventStore!!) -// -// val resultFromStateStore = tokenService.validLandscapeTokenValueAndSecret(expectedTokenValue, expectedSecret) -// assertTrue(resultFromStateStore, "Invalid token event in state store") -// -// // Now delete the event -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, null) -// -// val resultFromStateStore2 = tokenService.validLandscapeTokenValueAndSecret(expectedTokenValue, expectedSecret) -// assertFalse(resultFromStateStore2, "Invalid token event in state store, should be null") -// } -// -// @Test -// fun testFilteringTokenEventInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// EventType.values().forEach { eventType -> -// if (eventType != EventType.CREATED) { -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(eventType).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// } -// } -// -// assertEquals(0, tokenEventStore!!.approximateNumEntries(), "State store not empty, but should be empty") -// -// val tokenService = TokenService(tokenEventStore!!) -// -// val resultFromStateStore = tokenService.validLandscapeTokenValueAndSecret(expectedTokenValue, expectedSecret) -// assertFalse(resultFromStateStore, "Invalid token event in state store") -// } -} diff --git a/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt b/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt deleted file mode 100644 index 7c6bd40..0000000 --- a/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.confluent.kafka.schemaregistry.client.MockSchemaRegistryClient -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient -import io.quarkus.arc.profile.IfBuildProfile -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces - -@Dependent -class MockSchemaRegistryClientProducer { - - @Produces - @IfBuildProfile("test") - fun produceMockSchemaRegistry(): SchemaRegistryClient { - return MockSchemaRegistryClient() - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt b/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt deleted file mode 100644 index 55b426c..0000000 --- a/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.confluent.kafka.schemaregistry.avro.AvroSchema -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.quarkus.arc.profile.IfBuildProfile -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import java.io.IOException -import org.apache.avro.Schema -import org.eclipse.microprofile.config.inject.ConfigProperty - -/** Returns an injectable [SpecificAvroSerde]. */ -@Dependent -class MockSerdeStructureProducer -@Inject -constructor( - var registry: SchemaRegistryClient?, - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.spans") var outTopicSpans: String? = null -) { - - @Produces - @IfBuildProfile("test") - @Throws(IOException::class, RestClientException::class) - fun produceMockSpecificAvroSerde(): SpecificAvroSerde { - registry?.register( - "$outTopicSpans-value", - AvroSchema(Schema.Parser().parse(net.explorviz.avro.Span.`SCHEMA$`.toString())) - ) - - val valueSerde = SpecificAvroSerde(registry) - valueSerde.configure( - mapOf(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG to "http://registry:1234"), - false - ) - return valueSerde - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt b/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt deleted file mode 100644 index 0bf0964..0000000 --- a/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.confluent.kafka.schemaregistry.avro.AvroSchema -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.quarkus.arc.profile.IfBuildProfile -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import java.io.IOException -import net.explorviz.avro.TokenEvent -import org.apache.avro.Schema -import org.eclipse.microprofile.config.inject.ConfigProperty - -/** Returns an injectable {@link SpecificAvroSerde}. */ -@Dependent -class MockSerdeTokenEventProducer -@Inject -constructor( - var registry: SchemaRegistryClient?, - @ConfigProperty(name = "explorviz.kafka-streams.topics.in.tokens") var inTopicToken: String? = null -) { - @Produces - @IfBuildProfile("test") - @Throws(IOException::class, RestClientException::class) - fun produceMockSpecificAvroSerde(): SpecificAvroSerde? { - return if (inTopicToken != null && registry != null) { - registry?.register( - "${inTopicToken}-value", - AvroSchema(Schema.Parser().parse(net.explorviz.avro.TokenEvent.`SCHEMA$`.toString())) - ) - - val valueSerde = SpecificAvroSerde(registry) - valueSerde.configure( - mapOf(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG to "http://registry:1234"), - false - ) - valueSerde - } else { - null - } - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt b/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt deleted file mode 100644 index 08187fd..0000000 --- a/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt +++ /dev/null @@ -1,290 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import com.google.protobuf.ByteString -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.common.v1.KeyValue -import io.opentelemetry.proto.trace.v1.Span -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class AttributesReaderTest { - - companion object { - private const val KEY_LANDSCAPE_TOKEN = AttributesReader.LANDSCAPE_TOKEN - private const val KEY_LANDSCAPE_SECRET = AttributesReader.TOKEN_SECRET - private const val KEY_GIT_COMMIT_CHECKSUM = AttributesReader.GIT_COMMIT_CHECKSUM - private const val KEY_HOST_NAME = AttributesReader.HOST_NAME - private const val KEY_HOST_IP = AttributesReader.HOST_IP - private const val KEY_APPLICATION_NAME = AttributesReader.APPLICATION_NAME - private const val KEY_APPLICATION_INSTANCE_ID = AttributesReader.APPLICATION_INSTANCE_ID - private const val KEY_APPLICATION_LANGUAGE = AttributesReader.APPLICATION_LANGUAGE - private const val KEY_METHOD_FQN = AttributesReader.METHOD_FQN - private const val KEY_K8S_POD_NAME = AttributesReader.K8S_POD_NAME - private const val KEY_K8S_NAMESPACE = AttributesReader.K8S_NAMESPACE_NAME - private const val KEY_K8S_DEPLOYMENT = AttributesReader.K8S_DEPLOYMENT_NAME - private const val KEY_K8S_NODE = AttributesReader.K8S_NODE_NAME - - private const val TOKEN = "tok" - private const val SECRET = "secret" - private const val GIT_COMMIT_CHECKSUM = "gitCommitChecksum" - private const val HOSTNAME = "Host" - private const val HOST_IP = "1.2.3.4" - private const val APP_NAME = "Test App" - private const val APP_INSTANCE_ID = "1234L" - private const val APP_LANG = "java" - private const val FQN = "foo.bar.test()" - private const val NAME = "span name" - private const val K8S_POD_NAME = "my name is pod. james pod." - private const val K8S_NAMESPACE = "name " - private const val K8S_DEPLOYMENT = "deployment name" - private const val K8S_NODE = "node.js" - } - - @Test - fun testValidSpanReadOut() { - val attr = generateValidAttributesMap() - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertEquals(reader.landscapeToken, TOKEN) - assertEquals(reader.secret, SECRET) - assertEquals(reader.gitCommitChecksum, GIT_COMMIT_CHECKSUM) - assertEquals(reader.hostName, HOSTNAME) - assertEquals(reader.hostIpAddress, HOST_IP) - assertEquals(reader.applicationName, APP_NAME) - assertEquals(reader.applicationInstanceId, APP_INSTANCE_ID) - assertEquals(reader.applicationLanguage, APP_LANG) - assertEquals(reader.methodFqn, FQN) - assertExcept(reader, null) - } - - @Test - fun testDefaultTokenReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_LANDSCAPE_TOKEN, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.LANDSCAPE_TOKEN) - assertEquals(reader.landscapeToken, DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN) - } - - @Test - fun testDefaultSecretReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_LANDSCAPE_SECRET, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.TOKEN_SECRET) - assertEquals(reader.secret, DefaultAttributeValues.DEFAULT_LANDSCAPE_SECRET) - } - - @Test - fun testDefaultHostnameReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_HOST_NAME, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.HOST_NAME) - assertEquals(reader.hostName, DefaultAttributeValues.DEFAULT_HOST_NAME) - } - - @Test - fun testDefaultHostIpReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_HOST_IP, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.HOST_IP) - assertEquals(reader.hostIpAddress, DefaultAttributeValues.DEFAULT_HOST_IP) - } - - @Test - fun testDefaultAppNameReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_APPLICATION_NAME, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.APPLICATION_NAME) - assertEquals(reader.applicationName, DefaultAttributeValues.DEFAULT_APP_NAME) - } - - @Test - fun testDefaultAppInstanceIdReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_APPLICATION_INSTANCE_ID, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.APPLICATION_INSTANCE_ID) - assertEquals(reader.applicationInstanceId, DefaultAttributeValues.DEFAULT_APP_INSTANCE_ID) - } - - @Test - fun testDefaultAppLangReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.APPLICATION_LANGUAGE) - assertEquals(reader.applicationLanguage, DefaultAttributeValues.DEFAULT_APP_LANG) - } - - @Test - fun testDefaultFqnReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_METHOD_FQN, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.METHOD_FQN) - assertEquals(reader.methodFqn, DefaultAttributeValues.DEFAULT_CLASS_FQN + "." + span.getName()) - } - - @Test - fun testFqnFromNameReadOut() { - val outerPackage = "net" - val innerPackage = "explorviz" - val className = "Reader" - val methodName = "someNiceMethod()" - - // Only method name given - var span = Span.newBuilder().setName(methodName).build() - var reader = AttributesReader(span) - assertEquals(DefaultAttributeValues.DEFAULT_CLASS_FQN + "." + methodName, reader.methodFqn) - - // Class and method given - span = Span.newBuilder().setName("$className.$methodName").build() - reader = AttributesReader(span) - assertEquals("${DefaultAttributeValues.DEFAULT_PACKAGE_NAME}.${className}.${methodName}", reader.methodFqn) - - // Class and package given - span = Span.newBuilder().setName("$innerPackage.$className.$methodName").build() - reader = AttributesReader(span) - assertEquals("$innerPackage.$className.$methodName", reader.methodFqn) - - // Class and two packages given - span = Span.newBuilder().setName("$outerPackage.$innerPackage.$className.$methodName").build() - reader = AttributesReader(span) - assertEquals("$outerPackage.$innerPackage.$className.$methodName", reader.methodFqn) - } - - private fun assertExcept(reader: AttributesReader, except: String?) { - if (except != AttributesReader.LANDSCAPE_TOKEN) { - assertEquals(reader.landscapeToken, TOKEN) - } - if (except != AttributesReader.TOKEN_SECRET) { - assertEquals(reader.secret, SECRET) - } - if (except != AttributesReader.HOST_NAME) { - assertEquals(reader.hostName, HOSTNAME) - } - if (except != AttributesReader.HOST_IP) { - assertEquals(reader.hostIpAddress, HOST_IP) - } - if (except != AttributesReader.APPLICATION_NAME) { - assertEquals(reader.applicationName, APP_NAME) - } - if (except != AttributesReader.APPLICATION_INSTANCE_ID) { - assertEquals(reader.applicationInstanceId, APP_INSTANCE_ID) - } - if (except != AttributesReader.APPLICATION_LANGUAGE) { - assertEquals(reader.applicationLanguage, APP_LANG) - } - if (except != AttributesReader.METHOD_FQN) { - assertEquals(reader.methodFqn, FQN) - } - - // Kubernetes-related checks - if (except != AttributesReader.K8S_POD_NAME) { - assertEquals(reader.k8sPodName, K8S_POD_NAME) - } - if (except != AttributesReader.K8S_NAMESPACE_NAME) { - assertEquals(reader.k8sNamespace, K8S_NAMESPACE) - } - if (except != AttributesReader.K8S_DEPLOYMENT_NAME) { - assertEquals(reader.k8sDeploymentName, K8S_DEPLOYMENT) - } - if (except != AttributesReader.K8S_NODE_NAME) { - assertEquals(reader.k8sNodeName, K8S_NODE) - } - } - - private fun generateValidAttributesMap(): MutableList { - return mutableListOf( - KeyValue.newBuilder() - .setKey(KEY_LANDSCAPE_TOKEN) - .setValue(AnyValue.newBuilder().setStringValue(TOKEN).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_LANDSCAPE_SECRET) - .setValue(AnyValue.newBuilder().setStringValue(SECRET).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_GIT_COMMIT_CHECKSUM) - .setValue(AnyValue.newBuilder().setStringValue(GIT_COMMIT_CHECKSUM).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_HOST_NAME) - .setValue(AnyValue.newBuilder().setStringValue(HOSTNAME).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_HOST_IP) - .setValue(AnyValue.newBuilder().setStringValue(HOST_IP).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_APPLICATION_NAME) - .setValue(AnyValue.newBuilder().setStringValue(APP_NAME).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_APPLICATION_INSTANCE_ID) - .setValue(AnyValue.newBuilder().setStringValue(APP_INSTANCE_ID).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_APPLICATION_LANGUAGE) - .setValue(AnyValue.newBuilder().setStringValue(APP_LANG).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_METHOD_FQN) - .setValue(AnyValue.newBuilder().setStringValue(FQN).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_POD_NAME) - .setValue(AnyValue.newBuilder().setStringValue(K8S_POD_NAME).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_NAMESPACE) - .setValue(AnyValue.newBuilder().setStringValue(K8S_NAMESPACE).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_DEPLOYMENT) - .setValue(AnyValue.newBuilder().setStringValue(K8S_DEPLOYMENT).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_NODE) - .setValue(AnyValue.newBuilder().setStringValue(K8S_NODE).build()) - .build() - ) - } - - private fun removeElementAndReturnAttributesMap(key: String, map: MutableList): MutableList { - return map.filter { it.key != key }.toMutableList() - } - - private fun generateSpanFromAttributesMap(attributes: MutableList): Span { - return Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f9b9ae7a6", Charsets.UTF_8)) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charsets.UTF_8)) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charsets.UTF_8)) - .setName(NAME) - .setStartTimeUnixNano(1667986986000L) - .setEndTimeUnixNano(1667987046000L) - .addAllAttributes(attributes) - .build() - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt b/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt deleted file mode 100644 index 0822e87..0000000 --- a/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class DefaultAttributeValueTest { - - @Test - fun testDefaultTokenValue() { - assertEquals(DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN, "mytokenvalue") - } - - @Test - fun testDefaultTokenSecret() { - assertEquals(DefaultAttributeValues.DEFAULT_LANDSCAPE_SECRET, "mytokensecret") - } - - @Test - fun testDefaultHostIp() { - assertEquals(DefaultAttributeValues.DEFAULT_HOST_IP, "0.0.0.0") - } - - @Test - fun testDefaultHostName() { - assertEquals(DefaultAttributeValues.DEFAULT_HOST_NAME, "UNKNOWN-HOST") - } - - @Test - fun testDefaultAppName() { - assertEquals(DefaultAttributeValues.DEFAULT_APP_NAME, "UNKNOWN-APPLICATION") - } - - @Test - fun testDefaultInstanceId() { - assertEquals(DefaultAttributeValues.DEFAULT_APP_INSTANCE_ID, "0") - } - - @Test - fun testDefaultAppLang() { - assertEquals(DefaultAttributeValues.DEFAULT_APP_LANG, "UNKNOWN") - } - - @Test - fun testDefaultFqn() { - // This must adhere to the format , i.e., include at least two '.' - assertEquals(DefaultAttributeValues.DEFAULT_FQN, "unknownpkg.UnknownClass.unknownMethod") - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java b/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java deleted file mode 100644 index 3d62458..0000000 --- a/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package net.explorviz.trace.adapter.service.converter; - -import com.google.protobuf.ByteString; -import io.opentelemetry.proto.common.v1.AnyValue; -import io.opentelemetry.proto.common.v1.KeyValue; -import io.opentelemetry.proto.trace.v1.Span; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import java.util.HexFormat; -import net.explorviz.trace.persistence.PersistenceSpan; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class SpanConverterImplTest { - - @Inject - SpanConverterImpl spanConverter; - - private Span sampleSpan() { - return Span.newBuilder() - .setTraceId( - ByteString.copyFrom(HexFormat.of().parseHex("50c246ad9c9883d1558df9f19b9ae7a6"))) - .setSpanId(ByteString.copyFrom(HexFormat.of().parseHex("7ef83c66eabd5fbb"))) - .setParentSpanId(ByteString.copyFrom(HexFormat.of().parseHex("7ef83c66efe42aaa"))) - .addAttributes(stringAttr("explorviz.token.id", DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN)) - .addAttributes(stringAttr("git_commit_checksum", "gitchecksum")) - .addAttributes(stringAttr("host_address", "1.2.3.4")) - .addAttributes(stringAttr("host", "testhostname")) - .addAttributes(stringAttr("service.name", "testappname")) - .addAttributes(stringAttr("service.instance.id", "42")) - .addAttributes(stringAttr("telemetry.sdk.language", "java")) - .addAttributes(stringAttr("code.function.name", "net.explorviz.test.Class.doSomething")) - .setStartTimeUnixNano(1668069002431000000L) - .setEndTimeUnixNano(1668072086000000000L) - .build(); - } - - private KeyValue stringAttr(String key, String value) { - return KeyValue.newBuilder() - .setKey(key) - .setValue( - AnyValue.newBuilder() - .setStringValue(value) - .build() - ) - .build(); - } - - private PersistenceSpan resultSpan() { - return new PersistenceSpan(DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN, - "gitchecksum", - "7ef83c66eabd5fbb", - "7ef83c66efe42aaa", - "50c246ad9c9883d1558df9f19b9ae7a6", - 1668069002431000000L, - 1668072086000000000L, - "1.2.3.4", - "testhostname", - "testappname", - "java", - "42", - "net/explorviz/test/Class.java", - "doSomething", - "Class", - "", - "", - "", - ""); - } - - @Test - public void testSpanToPersistenceSpanConversion() { - final Span testSpan = this.sampleSpan(); - final PersistenceSpan expectedSpan = this.resultSpan(); - - PersistenceSpan resultSpan = spanConverter.fromOpenTelemetrySpan(testSpan); - - Assertions.assertEquals(expectedSpan, resultSpan); - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt b/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt deleted file mode 100644 index 80eebd1..0000000 --- a/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt +++ /dev/null @@ -1,261 +0,0 @@ -package net.explorviz.trace.adapter.service.validation - -import com.google.protobuf.ByteString -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.common.v1.KeyValue -import io.opentelemetry.proto.trace.v1.Span -import java.nio.charset.Charset -import net.explorviz.trace.adapter.service.TokenService -import net.explorviz.trace.adapter.service.converter.AttributesReader -import net.explorviz.trace.adapter.span.service.validation.DefaultSpanValidator -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.Mockito - -class DefaultSpanValidatorTest { - - private companion object { - const val KEY_LANDSCAPE_TOKEN = AttributesReader.LANDSCAPE_TOKEN - const val KEY_LANDSCAPE_SECRET = AttributesReader.TOKEN_SECRET - const val KEY_HOST_NAME = AttributesReader.HOST_NAME - const val KEY_HOST_IP = AttributesReader.HOST_IP - const val KEY_APPLICATION_NAME = AttributesReader.APPLICATION_NAME - const val KEY_APPLICATION_INSTANCE_ID = AttributesReader.APPLICATION_INSTANCE_ID - const val KEY_APPLICATION_LANGUAGE = AttributesReader.APPLICATION_LANGUAGE - const val KEY_METHOD_FQN = AttributesReader.METHOD_FQN - - const val TOKEN = "tok" - const val SECRET = "secret" - const val HOSTNAME = "Host" - const val HOST_IP = "1.2.3.4" - const val APP_NAME = "Test App" - const val APP_INSTANCE_ID = "1234L" - const val APP_LANG = "java" - const val FQN = "foo.bar.test()" - - fun newKeyValueString(key: String, value: String): KeyValue { - return KeyValue.newBuilder() - .setKey(key) - .setValue(AnyValue.newBuilder().setStringValue(value).build()) - .build() - } - } - - private lateinit var validator: DefaultSpanValidator - private lateinit var validSpan: AttributesReader - - @BeforeEach - fun setUp() { - val mockTokenService = Mockito.mock(TokenService::class.java) - Mockito.`when`(mockTokenService.validLandscapeTokenValueAndSecret(Mockito.anyString(), Mockito.anyString())) - .thenReturn(true) - - validator = DefaultSpanValidator(mockTokenService, true) - - validSpan = Mockito.mock(AttributesReader::class.java) - Mockito.`when`(validSpan.secret).thenReturn(SECRET) - Mockito.`when`(validSpan.landscapeToken).thenReturn(TOKEN) - Mockito.`when`(validSpan.hostName).thenReturn(HOSTNAME) - Mockito.`when`(validSpan.hostIpAddress).thenReturn(HOST_IP) - Mockito.`when`(validSpan.applicationName).thenReturn(APP_NAME) - Mockito.`when`(validSpan.applicationInstanceId).thenReturn(APP_INSTANCE_ID) - Mockito.`when`(validSpan.applicationLanguage).thenReturn(APP_LANG) - Mockito.`when`(validSpan.methodFqn).thenReturn(FQN) - } - - private fun generateValidAttributesMap(): List { - return listOf( - newKeyValueString(KEY_LANDSCAPE_TOKEN, TOKEN), - newKeyValueString(KEY_LANDSCAPE_SECRET, SECRET), - newKeyValueString(KEY_HOST_NAME, HOSTNAME), - newKeyValueString(KEY_HOST_IP, HOST_IP), - newKeyValueString(KEY_APPLICATION_NAME, APP_NAME), - newKeyValueString(KEY_APPLICATION_INSTANCE_ID, APP_INSTANCE_ID), - newKeyValueString(KEY_APPLICATION_LANGUAGE, APP_LANG), - newKeyValueString(KEY_METHOD_FQN, FQN), - ) - } - - private fun generateSpanFromAttributesMap(attributes: List): Span { - return Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(1667986986000L) - .setEndTimeUnixNano(1667987046000L) - .addAllAttributes(attributes) - .build() - } - - private fun removeElementAndReturnAttributesMap( - keyToBeRemoved: String, - attributes: List - ): List { - return attributes.filterNot { it.key == keyToBeRemoved } - } - - private fun replaceElementAndReturnAttributesMap( - key: String, - newVal: String, - attributes: List - ): List { - return attributes.map { if (it.key == key) newKeyValueString(key, newVal) else it } - } - - @Test - fun valid() { - val attrMap = generateValidAttributesMap() - val valid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(valid)) - } - - @Test - fun invalidLandscapeTokenValue() { - for (invalidTokenValue in listOf("", "\n", "\t", " ")) { - var attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_LANDSCAPE_TOKEN, invalidTokenValue, attrMap) - val invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - - @Test - fun invalidLandscapeTokenSecret() { - for (invalidTokenSecret in listOf("", "\n", "\t", " ")) { - var attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_LANDSCAPE_SECRET, invalidTokenSecret, attrMap) - val invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - - @Test - fun testHost() { - var attrMap = generateValidAttributesMap() - - attrMap = removeElementAndReturnAttributesMap(KEY_HOST_NAME, attrMap) - var invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = removeElementAndReturnAttributesMap(KEY_HOST_IP, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_HOST_IP, " ", attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - - for (invalidHostName in listOf("", "\n", "\t", " ")) { - for (invalidHostIp in listOf("", "\t", "\n", " ")) { - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_HOST_NAME, invalidHostName, attrMap) - attrMap = replaceElementAndReturnAttributesMap(KEY_HOST_IP, invalidHostIp, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - } - - @Test - fun testApp() { - var attrMap = generateValidAttributesMap() - - attrMap = removeElementAndReturnAttributesMap(KEY_APPLICATION_NAME, attrMap) - var invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = removeElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_NAME, " ", attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, " ", attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - - for (invalidId in listOf("", "\n", "\t", " ")) { - for (invalidLanguage in listOf("", "\t", "\n", " ")) { - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_NAME, invalidId, attrMap) - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, invalidLanguage, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - } - - @Test - fun testOperation() { - var attrMap = generateValidAttributesMap() - - val invalidValues = - listOf( - "", - "\n", - "\t", - " ", - "noMethod", - "...", - "a..", - "a.b.", - "a.b. ", - "a..c", - ".b.c", - "..c", - ".b.", - ) - for (invalidMethodFqn in invalidValues) { - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_METHOD_FQN, invalidMethodFqn, attrMap) - val invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - - @Test - fun testTimestamps() { - val attrMap = generateValidAttributesMap() - - val invalids = - listOf( - Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(0L) - .setEndTimeUnixNano(456L) - .addAllAttributes(attrMap) - .build(), - Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(456L) - .setEndTimeUnixNano(0L) - .addAllAttributes(attrMap) - .build(), - Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(0L) - .setEndTimeUnixNano(0L) - .addAllAttributes(attrMap) - .build(), - ) - - for (invalid in invalids) { - assertFalse(validator.isValid(invalid)) - } - } -} diff --git a/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt b/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt deleted file mode 100644 index 3643789..0000000 --- a/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.explorviz.trace.adapter.translation - -import com.google.common.io.BaseEncoding -import net.explorviz.trace.adapter.service.converter.IdHelper -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class IdHelperTest { - - @Test - fun spanId() { - val id = "7ef83c66eabd5fbb" - val spanId = BaseEncoding.base16().lowerCase().decode(id) - val convertedSpanId = IdHelper.convertSpanId(spanId) - assertEquals("7ef83c66eabd5fbb", convertedSpanId) - } - - @Test - fun traceId() { - val id = "50c246ad9c9883d1558df9f19b9ae7a6" - val traceId = BaseEncoding.base16().lowerCase().decode(id) - val convertedTraceId = IdHelper.convertTraceId(traceId) - assertEquals("50c246ad9c9883d1558df9f19b9ae7a6", convertedTraceId) - } -}