diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0fb50e3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,492 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.go,*.go2}] +indent_style = tab +ij_continuation_indent_size = 4 +ij_go_GROUP_CURRENT_PROJECT_IMPORTS = false +ij_go_add_leading_space_to_comments = false +ij_go_add_parentheses_for_single_import = false +ij_go_call_parameters_new_line_after_left_paren = true +ij_go_call_parameters_right_paren_on_new_line = true +ij_go_call_parameters_wrap = off +ij_go_fill_paragraph_width = 80 +ij_go_group_stdlib_imports = false +ij_go_import_sorting = gofmt +ij_go_keep_indents_on_empty_lines = false +ij_go_local_group_mode = project +ij_go_local_package_prefixes = +ij_go_move_all_imports_in_one_declaration = false +ij_go_move_all_stdlib_imports_in_one_group = false +ij_go_remove_redundant_import_aliases = false +ij_go_run_go_fmt_on_reformat = true +ij_go_use_back_quotes_for_imports = false +ij_go_wrap_comp_lit = off +ij_go_wrap_comp_lit_newline_after_lbrace = true +ij_go_wrap_comp_lit_newline_before_rbrace = true +ij_go_wrap_func_params = off +ij_go_wrap_func_params_newline_after_lparen = true +ij_go_wrap_func_params_newline_before_rparen = true +ij_go_wrap_func_result = off +ij_go_wrap_func_result_newline_after_lparen = true +ij_go_wrap_func_result_newline_before_rparen = true + +[{*.htm,*.html,*.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 + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.markdown,*.md}] +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_format_tables = 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 + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +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 diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index acf37f6..5ad34de 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,9 +1,15 @@ name: Build Docker images on: + workflow_dispatch: push: + branches: + - 'main' tags: - - '*' # Push events to matching tags + - 'v*' + pull_request: + branches: + - 'main' jobs: docker-api-cart: @@ -17,29 +23,39 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: alphayax/microservice-demo-cart-service + images: | + name=ghcr.io/${{ github.repository }}/cart-service tags: | + type=ref,event=branch + type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - + type=sha + type=raw,value=latest,enable={{is_default_branch}} + labels: | + maintainer=WeScale + org.opencontainers.image.title=cart-service + org.opencontainers.image.description=Example of cart-service microservice + org.opencontainers.image.vendor=WeScale - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./cart-service/ - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -54,29 +70,39 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: alphayax/microservice-demo-article-service + images: | + name=ghcr.io/${{ github.repository }}/article-service tags: | + type=ref,event=branch + type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - + type=sha + type=raw,value=latest,enable={{is_default_branch}} + labels: | + maintainer=WeScale + org.opencontainers.image.title=article-service + org.opencontainers.image.description=Example of article-service microservice + org.opencontainers.image.vendor=WeScale - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: - context: ./article-service/ - push: true + context: ./cart-service/ + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -91,29 +117,39 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: alphayax/microservice-demo-frontend-user + images: | + name=ghcr.io/${{ github.repository }}/frontend-user tags: | + type=ref,event=branch + type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - + type=sha + type=raw,value=latest,enable={{is_default_branch}} + labels: | + maintainer=WeScale + org.opencontainers.image.title=frontend-user + org.opencontainers.image.description=Example of frontend user portal + org.opencontainers.image.vendor=WeScale - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./front-user/ - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -128,28 +164,38 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: alphayax/microservice-demo-frontend-admin + images: | + name=ghcr.io/${{ github.repository }}/frontend-admin tags: | + type=ref,event=branch + type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - + type=semver,pattern={{major}} + type=sha + type=raw,value=latest,enable={{is_default_branch}} + labels: | + maintainer=WeScale + org.opencontainers.image.title=frontend-admin + org.opencontainers.image.description=Example of frontend admin portal + org.opencontainers.image.vendor=WeScale - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./front-admin/ - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78c1c9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,123 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template +# 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 + +# 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 + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Vuejs template +# Recommended template: Node.gitignore + +node_modules/ +dist/ +npm-debug.log +yarn-error.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5548450 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +## See https://pre-commit.com for more information +## See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-fmt + - id: go-lint + - id: validate-toml + - id: go-mod-tidy + - repo: https://github.com/pre-commit/mirrors-prettier + rev: 'v3.1.0' + hooks: + - id: prettier + types_or: [css, javascript, vue, ts, scss] + - repo: https://github.com/pre-commit/mirrors-eslint + rev: 'v8.56.0' + hooks: + - id: eslint + files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx + types: [ file ] diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..f3ab914 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +golang 1.20 diff --git a/article-service/example.http b/article-service/article-service.http similarity index 84% rename from article-service/example.http rename to article-service/article-service.http index 8e207fe..e9b77b0 100644 --- a/article-service/example.http +++ b/article-service/article-service.http @@ -14,6 +14,10 @@ Content-Type: application/json "description": "The best article of all times" } +### +GET http://localhost:8080/article/ +Accept: application/json + ### DELETE http://localhost:8080/article/655cabd93dea2ba2e45e3159/ Accept: application/json diff --git a/article-service/config.yaml b/article-service/config.yaml index b0c1f8d..28ed5b6 100644 --- a/article-service/config.yaml +++ b/article-service/config.yaml @@ -1,2 +1,4 @@ listen: ":8080" -mongodbUri: "mongodb://localhost:27017/alpha-articles" +mongodbURI: "mongodb://localhost:27017/alpha-articles" +logLevel: "debug" +environment: "dev" diff --git a/article-service/go.mod b/article-service/go.mod index 25fa41e..7e335e9 100644 --- a/article-service/go.mod +++ b/article-service/go.mod @@ -4,10 +4,12 @@ go 1.20 require ( github.com/gin-contrib/cors v1.4.0 + github.com/gin-contrib/requestid v0.0.6 github.com/gin-gonic/gin v1.9.1 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.17.0 + github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f go.mongodb.org/mongo-driver v1.13.0 ) @@ -22,6 +24,7 @@ require ( github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/snappy v0.0.1 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.0 // indirect diff --git a/article-service/go.sum b/article-service/go.sum index e160895..55b83ea 100644 --- a/article-service/go.sum +++ b/article-service/go.sum @@ -69,6 +69,8 @@ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= +github.com/gin-contrib/requestid v0.0.6 h1:mGcxTnHQ45F6QU5HQRgQUDsAfHprD3P7g2uZ4cSZo9o= +github.com/gin-contrib/requestid v0.0.6/go.mod h1:9i4vKATX/CdggbkY252dPVasgVucy/ggBeELXuQztm4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= @@ -148,6 +150,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -240,6 +244,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY= +github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= diff --git a/article-service/handler/AddArticle.go b/article-service/handler/AddArticle.go index f503f40..cc3e900 100644 --- a/article-service/handler/AddArticle.go +++ b/article-service/handler/AddArticle.go @@ -1,36 +1,64 @@ package handler import ( + "article-service/logger" "article-service/model" "article-service/repository" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "net/http" + + "github.com/gin-gonic/gin" ) +// AddArticle is a handler function for adding an article. +// It expects a JSON payload in the request body, representing an AddArticleRequest. +// If the request body is invalid or does not match the expected format, it returns a Bad Request response. +// If the AddArticleRequest fails validation, it returns a Bad Request response with validation error details. +// Otherwise, it adds the article to the repository, and returns a success response. func AddArticle(ctx *gin.Context) { + // Parse the JSON request body into an AddArticleRequest struct var addArticleRequest AddArticleRequest if err := ctx.ShouldBindJSON(&addArticleRequest); err != nil { + logger.Logger.Warnf(errorMsgFormat, "AddArticle", err.Error()) ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + logger.Logger.Infof( + "AddArticle - Adding % (%s) to database", + addArticleRequest.ArticleName, + addArticleRequest.ArticleDescription) + + // Validate the AddArticleRequest if err := addArticleRequest.validate(); err != nil { + logger.Logger.Warnf(errorMsgFormat, "AddArticle", err.Error()) ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + // Add the validated article to the repository err := repository.AddArticle(ctx, addArticleRequest.toArticle()) if err != nil { - log.Warnf("AddArticle Error: %s", err.Error()) + logger.Logger.Errorf(errorMsgFormat, "AddArticle", err.Error()) ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + + logger.Logger.Infof(successfulMsgFormat, "AddArticle", addArticleRequest.ArticleName) } +// AddArticleRequest represents the structure of a JSON request +// expected when adding an article. It includes fields for the +// article's name and description. The struct tags define the +// corresponding JSON key names for serialization and deserialization. type AddArticleRequest struct { - ArticleName string `json:"name"` + + // ArticleName is the name of the article. + // It is expected to be provided in the JSON request as "name". + ArticleName string `json:"name"` + + // ArticleDescription is the description of the article. + // It is expected to be provided in the JSON request as "description". ArticleDescription string `json:"description"` } diff --git a/article-service/handler/DeleteArticle.go b/article-service/handler/DeleteArticle.go index 4d53544..b2474e5 100644 --- a/article-service/handler/DeleteArticle.go +++ b/article-service/handler/DeleteArticle.go @@ -1,23 +1,36 @@ package handler import ( + "article-service/logger" "article-service/repository" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "net/http" + + "github.com/gin-gonic/gin" ) +// DeleteArticle is a handler function for deleting an article. +// It retrieves the article ID from the request parameters and attempts to delete the corresponding article. +// If an error occurs during the deletion process, it logs the error and returns an Internal Server Error response. +// Otherwise, it returns an Accepted response. func DeleteArticle(ctx *gin.Context) { - articleId := ctx.Param("articleId") + // Retrieve the article ID from the request parameters + articleID := ctx.Param("articleId") + + logger.Logger.Infof( + "DeleteArticle - Deleting article with ID %s", + articleID) - if err := repository.DeleteArticle(ctx, articleId); err != nil { - log.Warnf("DeleteArticle Error: %s", err.Error()) + // Attempt to delete the article from the repository + if err := repository.DeleteArticle(ctx, articleID); err != nil { + // Handle repository error, log the error, and return Internal Server Error response + logger.Logger.Errorf(errorMsgFormat, "DeleteArticle", err.Error()) ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), - "articleId": articleId, + "articleID": articleID, }) return } + logger.Logger.Infof(successfulMsgFormat, "DeleteArticle", articleID) ctx.Status(http.StatusAccepted) } diff --git a/article-service/handler/GetArticle.go b/article-service/handler/GetArticle.go index 18a8dc8..1dae2db 100644 --- a/article-service/handler/GetArticle.go +++ b/article-service/handler/GetArticle.go @@ -1,20 +1,31 @@ package handler import ( + "article-service/logger" "article-service/repository" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "net/http" + + "github.com/gin-gonic/gin" ) +// GetArticle is a handler function for retrieving articles. +// It retrieves a list of articles from the repository using GetArticles function. +// If an error occurs during the retrieval process, it returns an Internal Server Error response. +// Otherwise, it returns a JSON response containing the retrieved articles. func GetArticle(ctx *gin.Context) { + // Retrieve a list of articles from the repository + logger.Logger.Infof("GetArticle - Retrieve list of article from database") + articles, err := repository.GetArticles(ctx, nil) if err != nil { - log.Warnf("GetArticle Error: %s", err.Error()) + // Handle repository error, log the error, and return Internal Server Error response + logger.Logger.Errorf(errorMsgFormat, "GetArticle", err.Error()) ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + logger.Logger.Infof(successfulMsgFormat, "GetArticle", articles) + ctx.JSON(http.StatusOK, gin.H{ "articles": articles, }) diff --git a/article-service/handler/HealthZ.go b/article-service/handler/HealthZ.go index a8cdcdb..1a03af9 100644 --- a/article-service/handler/HealthZ.go +++ b/article-service/handler/HealthZ.go @@ -1,10 +1,34 @@ package handler import ( - "github.com/gin-gonic/gin" + "article-service/logger" + "article-service/repository" "net/http" + + "github.com/gin-gonic/gin" ) -func HealthZ(c *gin.Context) { - c.Status(http.StatusOK) +// HealthZ is a handler function for checking the health status of the application. +// It calls the PingDatabase function to check the connection to the database. +// If the database is reachable, it returns a JSON response with the status "mongodb: true" and HTTP status OK (200). +// If the database is not reachable, it returns a JSON response with the status "mongodb: false" and HTTP status Gateway Timeout (504). +func HealthZ(ctx *gin.Context) { + logger.Logger.Debugf("Healthz - checking database connection") + // Check the health status of the database by calling PingDatabase + if err := repository.PingDatabase(); err != nil { + // Handle database unavailability, return JSON response with "mongodb: false" and Gateway Timeout status + logger.Logger.Errorf(errorMsgFormat, "HealthZ", err.Error()) + + ctx.JSON(http.StatusGatewayTimeout, gin.H{ + "mongodb": false, + }) + return + } + + logger.Logger.Debugf(successfulMsgFormat, "Healthz", "OK") + + // Database is reachable, return JSON response with "mongodb: true" and OK status + ctx.JSON(http.StatusOK, gin.H{ + "mongodb": true, + }) } diff --git a/article-service/handler/common.go b/article-service/handler/common.go new file mode 100644 index 0000000..f6edefa --- /dev/null +++ b/article-service/handler/common.go @@ -0,0 +1,4 @@ +package handler + +const errorMsgFormat = "%s Error: %s" +const successfulMsgFormat = "%s successful - %s" diff --git a/article-service/logger/logging.go b/article-service/logger/logging.go new file mode 100644 index 0000000..48e1d9b --- /dev/null +++ b/article-service/logger/logging.go @@ -0,0 +1,34 @@ +package logger + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +// Logger is the global instance of the logrus.Logger used for logging in the application. +var Logger *logrus.Logger = logrus.New() + +// SetupLogging initializes the global Logger with the specified log level and JSON formatter. +// It reads the log level configuration from the environment variable LOG_LEVEL. +// If LOG_LEVEL is not set or is invalid, it defaults to "info" log level. +// The timestamp format for log entries is set to "2006-01-02 15:04:05Z". +func SetupLogging() { + // Read log level configuration from environment variable LOG_LEVEL + logLevel := os.Getenv("LOG_LEVEL") + + // Set default log level to info if LOG_LEVEL is not set or is invalid + if logLevel == "" { + logLevel = "info" + } + + // Parse the log level and set it for the global Logger + level, err := logrus.ParseLevel(logLevel) + if err != nil { + level = logrus.InfoLevel + } + Logger.SetLevel(level) + + // Set the formatter for the global Logger to JSON format with a specific timestamp format + Logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05Z"}) +} diff --git a/article-service/main.go b/article-service/main.go index b817071..f724df5 100644 --- a/article-service/main.go +++ b/article-service/main.go @@ -2,19 +2,24 @@ package main import ( "article-service/handler" + "article-service/logger" + "article-service/middlewares" "article-service/repository" + "time" + "github.com/gin-contrib/cors" + "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "time" ) func main() { - log.Infoln("-= Article Service =-") loadConfig() + logger.SetupLogging() + logger.Logger.Infoln("-= Article Service =-") initDatabase() - loadApiServer() + loadAPIServer() } // loadConfig define the default values and loads the user configuration from config.yaml @@ -25,6 +30,13 @@ func loadConfig() { if err != nil { log.Warnln(err) } + + viper.SetDefault("logLevel", "info") + err = viper.BindEnv("logLevel", "LOG_LEVEL") + if err != nil { + log.Warnln(err) + } + viper.SetConfigFile("config.yaml") viper.AddConfigPath("/etc/article-service/") if err := viper.ReadInConfig(); err != nil { @@ -34,17 +46,17 @@ func loadConfig() { // initDatabase initialize the database connection func initDatabase() { - mongodbUri := viper.GetString("mongodbUri") - if err := repository.Initialize(mongodbUri); err != nil { - log.Errorf("Failed to connect to %s", mongodbUri) - log.Panicln(err) + mongodbURI := viper.GetString("mongodbURI") + if err := repository.Initialize(mongodbURI); err != nil { + logger.Logger.Errorf("Failed to connect to %s", mongodbURI) + logger.Logger.Panicln(err) } - log.Infof("Connected to %s", mongodbUri) + logger.Logger.Infof("Connected to %s", mongodbURI) } -// loadApiServer initialize the API server with a cors middleware and define routes to be served. +// loadAPIServer initialize the API server with a cors middleware and define routes to be served. // This function is blocking: it will wait until the server returns an error -func loadApiServer() { +func loadAPIServer() { Router := gin.New() Router.Use(cors.New(cors.Config{ //AllowOrigins: []string{"http://localhost:3001"}, @@ -58,13 +70,13 @@ func loadApiServer() { MaxAge: 12 * time.Hour, })) - Router.Use( - gin.LoggerWithWriter(gin.DefaultWriter, "/", "/healthz"), - gin.Recovery(), - ) + Router.Use(middlewares.LoggingMiddleware(logger.Logger, "/", "/healthz")) + Router.Use(requestid.New()) + Router.Use(gin.Recovery()) Router.GET("/", handler.HealthZ) Router.GET("/healthz", handler.HealthZ) + Router.GET("/article/", handler.GetArticle) Router.POST("/article/", handler.AddArticle) Router.DELETE("/article/:articleId/", handler.DeleteArticle) diff --git a/article-service/middlewares/loggingMiddleware.go b/article-service/middlewares/loggingMiddleware.go new file mode 100644 index 0000000..0766c80 --- /dev/null +++ b/article-service/middlewares/loggingMiddleware.go @@ -0,0 +1,62 @@ +package middlewares + +import ( + "net/http" + "time" + + "github.com/gin-contrib/requestid" + "github.com/sirupsen/logrus" + + "github.com/gin-gonic/gin" +) + +// LoggingMiddleware instance a Logger middleware with the specified logrus instance. +func LoggingMiddleware(logger *logrus.Logger, notLoggedPaths ...string) gin.HandlerFunc { + + var skippedPaths map[string]bool + if length := len(notLoggedPaths); length > 0 { + skippedPaths = make(map[string]bool, length) + for _, currentPath := range notLoggedPaths { + skippedPaths[currentPath] = true + } + } + + return func(ctx *gin.Context) { + startTime := time.Now() + path := ctx.Request.URL.Path + + // Processing request + ctx.Next() + + if _, ok := skippedPaths[path]; !ok { + endTime := time.Now() + latencyTime := endTime.Sub(startTime) + reqMethod := ctx.Request.Method + requestURI := ctx.Request.RequestURI + statusCode := ctx.Writer.Status() + clientIP := ctx.ClientIP() + clientUserAgent := ctx.Request.UserAgent() + referer := ctx.Request.Referer() + + msg := logger.WithFields(logrus.Fields{ + "method": reqMethod, + "path": requestURI, + "statusCode": statusCode, + "latency": latencyTime, + "clientIP": clientIP, + "userAgent": clientUserAgent, + "referer": referer, + "requestId": requestid.Get(ctx), + }) + + switch { + case statusCode >= http.StatusInternalServerError: + msg.Error("Error when processing request") + case statusCode >= http.StatusBadRequest: + msg.Warn("Invalid request") + default: + msg.Info("processed Request with success") + } + } + } +} diff --git a/article-service/repository/Article.go b/article-service/repository/Article.go index a8c1b47..cb7d51a 100644 --- a/article-service/repository/Article.go +++ b/article-service/repository/Article.go @@ -1,8 +1,10 @@ package repository import ( + "article-service/logger" "article-service/model" "context" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" @@ -12,44 +14,71 @@ func collArticles() *mongo.Collection { return client.Database("article-alpha").Collection("article") } +// GetArticles retrieves a list of articles from the MongoDB collection based on the provided filter. +// If no filter is provided, it retrieves all articles from the collection. +// It returns the list of articles or an error. func GetArticles(ctx context.Context, filter bson.D) ([]*model.Article, error) { var articles []*model.Article + // Set an empty filter as default if none is provided if filter == nil { filter = bson.D{} } + // Query the MongoDB collection with the provided filter cursor, err := collArticles().Find(ctx, filter) if err != nil { + logger.Logger.Debugf("GetArticles - Error %s", err) return nil, err } + // Decode the query result into a list of articles if err := cursor.All(ctx, &articles); err != nil { + logger.Logger.Debugf("GetArticles - Error %s", err) return nil, err } + + logger.Logger.Debugf("GetArticles - Found %d articles", len(articles)) return articles, nil } +// AddArticle adds a new article to the MongoDB collection. +// It assigns a new ObjectID to the article before inserting it into the collection. +// It returns an error if any. func AddArticle(ctx context.Context, article *model.Article) error { + // Generate a new ObjectID for the article article.ID = primitive.NewObjectID().Hex() + + // Insert the article into the MongoDB collection _, err := collArticles().InsertOne(ctx, article) if err != nil { + logger.Logger.Debugf("AddArticle - Error %s", err) return err } + + logger.Logger.Debugf("AddArticle - Article inserted in the database with ID %s", article.ID) return nil } -func DeleteArticle(ctx context.Context, articleId string) error { +// DeleteArticle deletes an article from the MongoDB collection based on its ID. +// It returns an error if any. +func DeleteArticle(ctx context.Context, articleID string) error { var err error - objectId, err := primitive.ObjectIDFromHex(articleId) + // Convert the article ID to a MongoDB ObjectID + objectID, err := primitive.ObjectIDFromHex(articleID) if err != nil { + logger.Logger.Debugf("DeleteArticle - Error %s", err) return err } - _, err = collArticles().DeleteOne(ctx, bson.D{{"_id", objectId.Hex()}}) + // Delete the article from the MongoDB collection using its ObjectID + _, err = collArticles().DeleteOne(ctx, bson.D{{"_id", objectID.Hex()}}) if err != nil { + logger.Logger.Debugf("DeleteArticle - Error %s", err) return err } + + logger.Logger.Debugf("DeleteArticle - Article with ID %s deleted", articleID) return nil } diff --git a/article-service/repository/util.go b/article-service/repository/util.go index cf594ba..b7b0750 100644 --- a/article-service/repository/util.go +++ b/article-service/repository/util.go @@ -1,31 +1,62 @@ package repository import ( + "article-service/logger" "context" + "time" + "github.com/pkg/errors" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "time" ) var client *mongo.Client -// Initialize and check the database connection -func Initialize(mongoDbUri string) error { +// Initialize establishes a connection to the MongoDB database using the provided URI. +// It sets a timeout of 5 seconds for the connection attempt and returns an error if the +// connection or ping to the database fails. +func Initialize(mongoDbURI string) error { + // Set a 5-second timeout for the connection attempt ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - clientOptions := options.Client().ApplyURI(mongoDbUri) + // Configure the MongoDB client with the provided URI + clientOptions := options.Client().ApplyURI(mongoDbURI) var err error + + // Connect to the MongoDB database client, err = mongo.Connect(ctx, clientOptions) if err != nil { + logger.Logger.Errorf("Initialize - Unable to initialize the database connection - %s ", err.Error()) return errors.Wrap(err, "Unable to initialize the database connection") } + // Ping the database to verify the connection err = client.Ping(ctx, nil) if err != nil { + logger.Logger.Errorf("Initialize - Unable to ping the database - %s ", err.Error()) + return errors.Wrap(err, "Unable to ping the database") + } + + logger.Logger.Info("Initialize - initialization successful") + return nil +} + +// PingDatabase checks the health of the existing MongoDB database connection. +// It sets a timeout of 5 seconds for the ping attempt and returns an error if the +// ping to the database fails. +func PingDatabase() error { + // Set a 5-second timeout for the ping attempt + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Ping the database to verify the connection + err := client.Ping(ctx, nil) + if err != nil { + logger.Logger.Errorf("Ping - ", err.Error()) return errors.Wrap(err, "Unable to ping the database") } + logger.Logger.Debug("Ping - OK") return nil } diff --git a/cart-service/cart-service.http b/cart-service/cart-service.http index 8738b28..c6faedf 100644 --- a/cart-service/cart-service.http +++ b/cart-service/cart-service.http @@ -1,8 +1,8 @@ -GET http://localhost:8080/ +GET http://localhost:8081/ Accept: application/json ### Health probe -GET http://localhost:8080/healthz +GET http://localhost:8081/healthz Accept: application/json ### Get cart with id=1 diff --git a/cart-service/config.yaml b/cart-service/config.yaml new file mode 100644 index 0000000..afdf724 --- /dev/null +++ b/cart-service/config.yaml @@ -0,0 +1,4 @@ +listen: ":8081" +redisURI: "redis://localhost:6379" +logLevel: "debug" +environment: "dev" diff --git a/cart-service/go.mod b/cart-service/go.mod index 767f46b..5b26b87 100644 --- a/cart-service/go.mod +++ b/cart-service/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/gin-contrib/cors v1.4.0 + github.com/gin-contrib/requestid v0.0.6 github.com/gin-gonic/gin v1.9.1 github.com/pkg/errors v0.9.1 github.com/redis/go-redis/v9 v9.3.0 @@ -23,6 +24,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect diff --git a/cart-service/go.sum b/cart-service/go.sum index 2c73411..fa1995f 100644 --- a/cart-service/go.sum +++ b/cart-service/go.sum @@ -75,6 +75,8 @@ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= +github.com/gin-contrib/requestid v0.0.6 h1:mGcxTnHQ45F6QU5HQRgQUDsAfHprD3P7g2uZ4cSZo9o= +github.com/gin-contrib/requestid v0.0.6/go.mod h1:9i4vKATX/CdggbkY252dPVasgVucy/ggBeELXuQztm4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= @@ -152,6 +154,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= diff --git a/cart-service/handler/DeleteCart.go b/cart-service/handler/DeleteCart.go index 4cdf943..9bbc284 100644 --- a/cart-service/handler/DeleteCart.go +++ b/cart-service/handler/DeleteCart.go @@ -1,19 +1,32 @@ package handler import ( - "cart-service/reporitory" - "github.com/gin-gonic/gin" + "cart-service/logger" + "cart-service/repository" "net/http" + + "github.com/gin-gonic/gin" ) +// DeleteCart is a handler function for deleting a cart. +// It retrieves the cart ID from the request parameters and attempts to delete the corresponding cart. +// If an error occurs during the deletion process, it returns an Internal Server Error response. +// Otherwise, it returns an Accepted response indicating the successful deletion of the cart. func DeleteCart(ctx *gin.Context) { - cartId := ctx.Param("cartId") + // Retrieve the cart ID from the request parameters + cartID := ctx.Param("cartId") + logger.Logger.Infof("DeleteCart - Delete cart with ID %s from database", cartID) - err := reporitory.DeleteCart(ctx, cartId) + // Attempt to delete the cart using the DeleteCart function from the repository + err := repository.DeleteCart(ctx, cartID) if err != nil { + // Handle repository error and return Internal Server Error response + logger.Logger.Errorf("DeleteCart with cartId %s - error - %s", cartID, err.Error()) ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + // Return Accepted response indicating the successful deletion of the cart + logger.Logger.Infof("DeleteCart with cartId %s - successful", cartID) ctx.Status(http.StatusAccepted) } diff --git a/cart-service/handler/GetCart.go b/cart-service/handler/GetCart.go index 820d2e6..8b313f5 100644 --- a/cart-service/handler/GetCart.go +++ b/cart-service/handler/GetCart.go @@ -1,30 +1,47 @@ package handler import ( + "cart-service/logger" "cart-service/model" - "cart-service/reporitory" - "github.com/gin-gonic/gin" + "cart-service/repository" "net/http" + + "github.com/gin-gonic/gin" ) +// GetCart is a handler function for retrieving a cart. +// It retrieves the cart ID from the request parameters and attempts to retrieve the corresponding cart from the repository. +// If an error occurs during the retrieval process, it returns an Internal Server Error response. +// If the cart is not found in the repository, a new cart with the specified ID is created. +// The retrieved or newly created cart is then returned in a JSON response. func GetCart(ctx *gin.Context) { - cartId := ctx.Param("cartId") + // Retrieve the cart ID from the request parameters + cartID := ctx.Param("cartId") + + logger.Logger.Infof("GetCart - Retrieve cart with ID %s from database", cartID) - cart, err := reporitory.GetCart(ctx, cartId) + // Attempt to retrieve the cart using the GetCart function from the repository + cart, err := repository.GetCart(ctx, cartID) if err != nil { + logger.Logger.Errorf("GetCart - Cart with ID %s - error - %s", cartID, err.Error()) + // Handle repository error and return Internal Server Error response ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } - // If cart is nil, create a new one + // If the cart is not found in the repository, create a new cart with the specified ID if cart == nil { + logger.Logger.Debugf("GetCart - Cart with ID %s - not found", cartID) + logger.Logger.Debugf("GetCart - Cart with ID %s - create an empty one", cartID) cart = &model.Cart{ - Id: cartId, + ID: cartID, } } + logger.Logger.Infof("GetCart - Cart with ID %s - successful", cartID) + // Return a JSON response with the retrieved or newly created cart ctx.JSON(http.StatusOK, gin.H{ "cart": cart, }) diff --git a/cart-service/handler/HealthZ.go b/cart-service/handler/HealthZ.go index a8cdcdb..91abd47 100644 --- a/cart-service/handler/HealthZ.go +++ b/cart-service/handler/HealthZ.go @@ -1,10 +1,34 @@ package handler import ( - "github.com/gin-gonic/gin" + "cart-service/logger" + "cart-service/repository" "net/http" + + "github.com/gin-gonic/gin" ) -func HealthZ(c *gin.Context) { - c.Status(http.StatusOK) +// HealthZ is a handler function for checking the health status of the application. +// It calls the PingDatabase function to check the connection to the database. +// If the database is reachable, it returns a JSON response with the status "mongodb: true" and HTTP status OK (200). +// If the database is not reachable, it returns a JSON response with the status "mongodb: false" and HTTP status Gateway Timeout (504). +func HealthZ(ctx *gin.Context) { + logger.Logger.Debugf("Healthz - checking database connection") + // Check the health status of the database by calling PingDatabase + if err := repository.PingDatabase(); err != nil { + // Handle database unavailability, return JSON response with "mongodb: false" and Gateway Timeout status + logger.Logger.Errorf("HealthZ - %s", err.Error()) + + ctx.JSON(http.StatusGatewayTimeout, gin.H{ + "redis": false, + }) + return + } + + logger.Logger.Debugf("Healthz - OK") + + // Database is reachable, return JSON response with "mongodb: true" and OK status + ctx.JSON(http.StatusOK, gin.H{ + "redis": true, + }) } diff --git a/cart-service/handler/UpdateCart.go b/cart-service/handler/UpdateCart.go index 195abe6..a5a861f 100644 --- a/cart-service/handler/UpdateCart.go +++ b/cart-service/handler/UpdateCart.go @@ -1,29 +1,54 @@ package handler import ( - "cart-service/reporitory" - "github.com/gin-gonic/gin" + "cart-service/logger" + "cart-service/repository" "net/http" + + "github.com/gin-gonic/gin" ) +// UpdateCart is a handler function for updating a cart with new items. +// It retrieves the cart ID from the request parameters and the items to update from the JSON request body. +// If the request body is invalid or does not match the expected format, it returns a Bad Request response. +// It then calls the UpdateCart function from the repository to update the cart with the specified items. +// If an error occurs during the update process, it returns an Internal Server Error response. +// Otherwise, it returns an Accepted response indicating the successful update of the cart. func UpdateCart(ctx *gin.Context) { - cartId := ctx.Param("cartId") + // Retrieve the cart ID from the request parameters + cartID := ctx.Param("cartId") + logger.Logger.Infof("UpdateCart - Update cart with ID %s in database", cartID) + // Parse the JSON request body into an UpdateCartRequest struct var updateCartRequest UpdateCartRequest if err := ctx.ShouldBindJSON(&updateCartRequest); err != nil { + logger.Logger.Warnf("UpdateCart with cartId %s - error - %s", cartID, err.Error()) + + // Handle JSON binding error and return Bad Request response ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - err := reporitory.UpdateCart(ctx, cartId, updateCartRequest.Items) + // Call the UpdateCart function from the repository to update the cart with the specified items + err := repository.UpdateCart(ctx, cartID, updateCartRequest.Items) if err != nil { + logger.Logger.Errorf("UpdateCart with cartId %s - error - %s", cartID, err.Error()) + + // Handle repository error and return Internal Server Error response ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + logger.Logger.Infof("UpdateCart with cartId %s - successful", cartID) + // Return Accepted response indicating the successful update of the cart ctx.Status(http.StatusAccepted) } +// UpdateCartRequest represents the structure of a JSON request +// expected when updating a cart. It includes a list of items to be updated. +// The struct tags define the corresponding JSON key names for serialization and deserialization. type UpdateCartRequest struct { + // Items is a list of strings representing the items to be updated in the cart. + // It is expected to be provided in the JSON request as "items". Items []string `json:"items"` } diff --git a/cart-service/logger/logging.go b/cart-service/logger/logging.go new file mode 100644 index 0000000..48e1d9b --- /dev/null +++ b/cart-service/logger/logging.go @@ -0,0 +1,34 @@ +package logger + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +// Logger is the global instance of the logrus.Logger used for logging in the application. +var Logger *logrus.Logger = logrus.New() + +// SetupLogging initializes the global Logger with the specified log level and JSON formatter. +// It reads the log level configuration from the environment variable LOG_LEVEL. +// If LOG_LEVEL is not set or is invalid, it defaults to "info" log level. +// The timestamp format for log entries is set to "2006-01-02 15:04:05Z". +func SetupLogging() { + // Read log level configuration from environment variable LOG_LEVEL + logLevel := os.Getenv("LOG_LEVEL") + + // Set default log level to info if LOG_LEVEL is not set or is invalid + if logLevel == "" { + logLevel = "info" + } + + // Parse the log level and set it for the global Logger + level, err := logrus.ParseLevel(logLevel) + if err != nil { + level = logrus.InfoLevel + } + Logger.SetLevel(level) + + // Set the formatter for the global Logger to JSON format with a specific timestamp format + Logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05Z"}) +} diff --git a/cart-service/main.go b/cart-service/main.go index 4f00452..861c5b0 100644 --- a/cart-service/main.go +++ b/cart-service/main.go @@ -2,19 +2,24 @@ package main import ( "cart-service/handler" - "cart-service/reporitory" + "cart-service/logger" + "cart-service/middlewares" + "cart-service/repository" + "time" + "github.com/gin-contrib/cors" + "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "time" ) func main() { - log.Infoln("-= Cart service =-") loadConfig() + logger.SetupLogging() + logger.Logger.Infoln("-= Cart service =-") initDatabase() - loadApiServer() + loadAPIServer() } // loadConfig define the default values and loads the user configuration from config.yaml @@ -22,29 +27,36 @@ func loadConfig() { viper.SetDefault("listen", ":8081") viper.SetDefault("redisUri", "redis://localhost:6379") err := viper.BindEnv("redisUri", "REDIS_URI") + if err != nil { + logger.Logger.Warnln(err) + } + + viper.SetDefault("logLevel", "info") + err = viper.BindEnv("logLevel", "LOG_LEVEL") if err != nil { log.Warnln(err) } + viper.SetConfigFile("config.yaml") viper.AddConfigPath("/etc/article-cart/") if err := viper.ReadInConfig(); err != nil { - log.Warnln(err) + logger.Logger.Warnln(err) } } // initDatabase initialize the database connection func initDatabase() { - redisUri := viper.GetString("redisUri") - if err := reporitory.Initialize(redisUri); err != nil { - log.Errorf("Failed to connect to %s", redisUri) - log.Panicln(err) + redisURI := viper.GetString("redisURI") + if err := repository.Initialize(redisURI); err != nil { + logger.Logger.Errorf("Failed to connect to %s", redisURI) + logger.Logger.Panicln(err) } - log.Infof("Connected to %s", redisUri) + logger.Logger.Infof("Connected to %s", redisURI) } -// loadApiServer initialize the API server with a cors middleware and define routes to be served. +// loadAPIServer initialize the API server with a cors middleware and define routes to be served. // This function is blocking: it will wait until the server returns an error -func loadApiServer() { +func loadAPIServer() { Router := gin.New() Router.Use(cors.New(cors.Config{ //AllowOrigins: []string{"http://localhost:3001"}, @@ -59,7 +71,8 @@ func loadApiServer() { })) Router.Use( - gin.LoggerWithWriter(gin.DefaultWriter, "/", "/healthz"), + middlewares.LoggingMiddleware(logger.Logger, "/", "/healthz"), + requestid.New(), gin.Recovery(), ) diff --git a/cart-service/middlewares/loggingMiddleware.go b/cart-service/middlewares/loggingMiddleware.go new file mode 100644 index 0000000..0766c80 --- /dev/null +++ b/cart-service/middlewares/loggingMiddleware.go @@ -0,0 +1,62 @@ +package middlewares + +import ( + "net/http" + "time" + + "github.com/gin-contrib/requestid" + "github.com/sirupsen/logrus" + + "github.com/gin-gonic/gin" +) + +// LoggingMiddleware instance a Logger middleware with the specified logrus instance. +func LoggingMiddleware(logger *logrus.Logger, notLoggedPaths ...string) gin.HandlerFunc { + + var skippedPaths map[string]bool + if length := len(notLoggedPaths); length > 0 { + skippedPaths = make(map[string]bool, length) + for _, currentPath := range notLoggedPaths { + skippedPaths[currentPath] = true + } + } + + return func(ctx *gin.Context) { + startTime := time.Now() + path := ctx.Request.URL.Path + + // Processing request + ctx.Next() + + if _, ok := skippedPaths[path]; !ok { + endTime := time.Now() + latencyTime := endTime.Sub(startTime) + reqMethod := ctx.Request.Method + requestURI := ctx.Request.RequestURI + statusCode := ctx.Writer.Status() + clientIP := ctx.ClientIP() + clientUserAgent := ctx.Request.UserAgent() + referer := ctx.Request.Referer() + + msg := logger.WithFields(logrus.Fields{ + "method": reqMethod, + "path": requestURI, + "statusCode": statusCode, + "latency": latencyTime, + "clientIP": clientIP, + "userAgent": clientUserAgent, + "referer": referer, + "requestId": requestid.Get(ctx), + }) + + switch { + case statusCode >= http.StatusInternalServerError: + msg.Error("Error when processing request") + case statusCode >= http.StatusBadRequest: + msg.Warn("Invalid request") + default: + msg.Info("processed Request with success") + } + } + } +} diff --git a/cart-service/model/Cart.go b/cart-service/model/Cart.go index b2c0c38..6f7dda7 100644 --- a/cart-service/model/Cart.go +++ b/cart-service/model/Cart.go @@ -2,17 +2,29 @@ package model import "encoding/json" +// Cart represents the structure of a shopping cart. +// It includes an ID and a list of items in the cart. +// The struct tags define the corresponding JSON key names for serialization and deserialization. type Cart struct { - Id string `json:"id"` + // ID is the unique identifier of the cart. + // It is expected to be provided in the JSON representation as "id". + ID string `json:"id"` + + // Items is a list of strings representing the items in the cart. + // It is expected to be provided in the JSON representation as "items". Items []string `json:"items"` } +// DecodeCart parses a JSON representation of a cart from a string. +// It returns a pointer to a Cart struct and an error, if any. func DecodeCart(rawCart string) (*Cart, error) { cart := &Cart{} err := json.Unmarshal([]byte(rawCart), cart) return cart, err } +// EncodeCart converts a Cart struct into its JSON representation as a string. +// It returns the JSON string and an error, if any. func EncodeCart(cart *Cart) (string, error) { rawCart, err := json.Marshal(cart) return string(rawCart), err diff --git a/cart-service/reporitory/Cart.go b/cart-service/reporitory/Cart.go deleted file mode 100644 index cfa1a49..0000000 --- a/cart-service/reporitory/Cart.go +++ /dev/null @@ -1,92 +0,0 @@ -package reporitory - -import ( - "cart-service/model" - "context" - "github.com/redis/go-redis/v9" -) - -func GetCart(ctx context.Context, cartId string) (cart *model.Cart, err error) { - cartRaw, err := client.Get(ctx, cartId).Result() - if err != nil { - if err == redis.Nil { - return nil, nil - } - return nil, err - } - - cart, err = model.DecodeCart(cartRaw) - if err != nil { - return nil, err - } - - return cart, nil -} - -func UpdateCart(ctx context.Context, cartId string, items []string) error { - cart, err := GetCart(ctx, cartId) - if err != nil { - return err - } - - if cart == nil { - cart = &model.Cart{ - Id: cartId, - Items: []string{}, - } - } - - cart.Items = items - - cartRaw, err := model.EncodeCart(cart) - if err != nil { - return err - } - - err = client.Set(ctx, cartId, cartRaw, 0).Err() - if err != nil { - return err - } - - return nil -} - -func DeleteCart(ctx context.Context, cartId string) error { - err := client.Del(ctx, cartId).Err() - if err != nil { - return err - } - - return nil -} - -/* -// Future implementation -func AddToCart(ctx context.Context, cartId string, itemId string) error { - cart, err := GetCart(ctx, cartId) - if err != nil { - return err - } - - if cart == nil { - cart = &model.Cart{ - Id: cartId, - Items: []string{}, - } - } - - cart.Items = append(cart.Items, itemId) - - cartRaw, err := model.EncodeCart(cart) - if err != nil { - return err - } - - err = client.Set(ctx, cartId, cartRaw, 0).Err() - if err != nil { - return err - } - - return nil -} -*/ diff --git a/cart-service/reporitory/util.go b/cart-service/reporitory/util.go deleted file mode 100644 index 9cbbe1a..0000000 --- a/cart-service/reporitory/util.go +++ /dev/null @@ -1,18 +0,0 @@ -package reporitory - -import ( - "github.com/pkg/errors" - "github.com/redis/go-redis/v9" -) - -var client *redis.Client - -func Initialize(redisUri string) error { - opt, err := redis.ParseURL(redisUri) - if err != nil { - return errors.Wrap(err, "Unable to initialize the redis connection") - } - - client = redis.NewClient(opt) - return nil -} diff --git a/cart-service/repository/Cart.go b/cart-service/repository/Cart.go new file mode 100644 index 0000000..f911eb5 --- /dev/null +++ b/cart-service/repository/Cart.go @@ -0,0 +1,142 @@ +package repository + +import ( + "cart-service/logger" + "cart-service/model" + "context" + + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +// GetCart retrieves a cart from the Redis cache using the provided cart ID. +// If the cart is not found in the cache, it returns nil and no error. +// If an error occurs during the retrieval process, it returns the error. +func GetCart(ctx context.Context, cartID string) (cart *model.Cart, err error) { + // Retrieve the raw cart data from the Redis cache + cartRaw, err := client.Get(ctx, cartID).Result() + if err != nil { + // Handle the case where the cart is not found in the cache + if errors.Is(err, redis.Nil) { + logger.Logger.Debugf("GetCart - Cart with ID %s not found in cache", cartID) + return nil, nil + } + logger.Logger.Debugf("GetCart - Cart with ID %s - Error %s", cartID, err) + return nil, err + } + + logger.Logger.Debugf("GetCart - Cart with ID %s found in cache", cartID) + // Decode the raw cart data into a Cart struct + cart, err = model.DecodeCart(cartRaw) + if err != nil { + logger.Logger.Debugf("GetCart - Cart with ID %s - cannot decode cart - Error %s", cartID, err) + return nil, err + } + + logger.Logger.Debugf("GetCart - Cart with ID %s - cart %s", cartID, cart) + return cart, nil +} + +// UpdateCart updates a cart in the Redis cache with the specified items. +// It retrieves the current cart, updates the items, encodes the updated cart, and stores it back in the cache. +// If the cart does not exist, a new cart is created with the provided cart ID. +// If an error occurs during the update process, it returns the error. +func UpdateCart(ctx context.Context, cartID string, items []string) error { + // Retrieve the current cart from the cache + cart, err := GetCart(ctx, cartID) + if err != nil { + logger.Logger.Debugf("UpdateCart - Cart with ID %s - Error %s", cartID, err) + return err + } + + // If the cart does not exist, create a new one with the provided cart ID + if cart == nil { + logger.Logger.Debugf("UpdateCart - Cart with ID %s not found - create an empty one", cartID) + cart = &model.Cart{ + ID: cartID, + Items: []string{}, + } + } + + // Update the cart items + cart.Items = items + + // Encode the updated cart + cartRaw, err := model.EncodeCart(cart) + if err != nil { + logger.Logger.Debugf("UpdateCart - Cart with ID %s - cannot encode cart - Error %s", cartID, err) + return err + } + + // Store the updated cart back in the Redis cache + err = client.Set(ctx, cartID, cartRaw, 0).Err() + if err != nil { + logger.Logger.Debugf("UpdateCart - Cart with ID %s - Error %s", cartID, err) + return err + } + logger.Logger.Debugf("UpdateCart - Cart with ID %s - update successful", cartID) + + return nil +} + +// DeleteCart removes a cart from the Redis cache using the provided cart ID. +// If an error occurs during the deletion process, it returns the error. +func DeleteCart(ctx context.Context, cartID string) error { + // Delete the cart from the Redis cache + err := client.Del(ctx, cartID).Err() + if err != nil { + logger.Logger.Debugf("DeleteCart - Cart with ID %s - Error %s", cartID, err) + return err + } + + logger.Logger.Debugf("DeleteCart - Cart with ID %s - delete successful", cartID) + return nil +} + +// AddToCart adds an item to the cart in the Redis cache using the provided cart ID. +// It retrieves the current cart, appends the new item, encodes the updated cart, and stores it back in the cache. +// If the cart does not exist, a new cart is created with the provided cart ID. +// If an error occurs during the process, it returns the error. +func AddToCart(ctx context.Context, cartID string, itemID string) error { + // Retrieve the current cart from the cache + cart, err := GetCart(ctx, cartID) + if err != nil { + logger.Logger.Debugf("AddToCart - Cart with ID %s, Article with ID %s - Error %s", cartID, itemID, err) + return err + } + + // If the cart does not exist, create a new one with the provided cart ID + if cart == nil { + logger.Logger.Debugf("AddToCart - Cart with ID %s not found - create an empty one", cartID) + cart = &model.Cart{ + ID: cartID, + Items: []string{}, + } + } + + // Append the new item to the cart + cart.Items = append(cart.Items, itemID) + + // Encode the updated cart + cartRaw, err := model.EncodeCart(cart) + if err != nil { + logger.Logger.Debugf( + "AddToCart - Cart with ID %s, Article with ID %s - cannot encode cart - Error %s", + cartID, + err) + return err + } + + // Store the updated cart back in the Redis cache + err = client.Set(ctx, cartID, cartRaw, 0).Err() + if err != nil { + logger.Logger.Debugf( + "AddToCart - Cart with ID %s, Article with ID %s - cannot store cart - Error %s", + cartID, + err) + return err + } + + logger.Logger.Debugf("UpdateCart - Cart with ID %s - update successful", cartID) + return nil +} diff --git a/cart-service/repository/util.go b/cart-service/repository/util.go new file mode 100644 index 0000000..80a605a --- /dev/null +++ b/cart-service/repository/util.go @@ -0,0 +1,46 @@ +package repository + +import ( + "cart-service/logger" + "context" + "time" + + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +var client *redis.Client + +// Initialize establishes a connection to the Redis server using the provided URI. +// It parses the URI, creates a new Redis client, and stores it in the global 'client' variable. +// If an error occurs during the initialization process, it returns the error. +func Initialize(redisURI string) error { + // Parse the Redis URI + opt, err := redis.ParseURL(redisURI) + if err != nil { + return errors.Wrap(err, "Unable to initialize the Redis connection") + } + + // Create a new Redis client and store it in the global 'client' variable + client = redis.NewClient(opt) + return nil +} + +// PingDatabase checks the health of the existing MongoDB database connection. +// It sets a timeout of 5 seconds for the ping attempt and returns an error if the +// ping to the database fails. +func PingDatabase() error { + // Set a 5-second timeout for the ping attempt + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Ping the database to verify the connection + err := client.Ping(ctx).Err() + if err != nil { + logger.Logger.Errorf("Ping - Unable to ping the database - %s", err.Error()) + return errors.Wrap(err, "Unable to ping the database") + } + + logger.Logger.Debug("Ping - OK") + return nil +} diff --git a/docker-compose.yml b/docker-compose.yml index c6387d5..fcc79f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,12 @@ services: mongo: image: mongo:4 - + ports: + - "27017:27017" redis: image: redis:6 + ports: + - "6379:6379" api-cart: build: