Làm cách nào tôi có thể có đầu ra nhật ký trình ghi ruby ​​cho stdout cũng như tệp?


94

Một số như một chức năng phát bóng trong trình ghi nhật ký.


1
Thêm | teetrước khi tệp làm việc cho tôi, vì vậy Logger.new("| tee test.log"). Lưu ý đường ống. Đây là từ một mẹo trên coderwall.com/p/y_b3ra/...
Mike W

@mjwatts Sử dụng tee --append test.logđể ngăn ghi đè.
fangxing

Câu trả lời:


124

Bạn có thể viết một IOlớp giả sẽ ghi cho nhiều IOđối tượng. Cái gì đó như:

class MultiIO
  def initialize(*targets)
     @targets = targets
  end

  def write(*args)
    @targets.each {|t| t.write(*args)}
  end

  def close
    @targets.each(&:close)
  end
end

Sau đó, đặt nó làm tệp nhật ký của bạn:

log_file = File.open("log/debug.log", "a")
Logger.new MultiIO.new(STDOUT, log_file)

Mỗi khi Loggergọi đối tượng putscủa bạn MultiIO, nó sẽ ghi vào cả hai STDOUTvà tệp nhật ký của bạn.

Chỉnh sửa: Tôi đã tiếp tục và tìm ra phần còn lại của giao diện. Thiết bị ghi nhật ký phải phản hồi writeclose(không puts). Miễn là MultiIOphản hồi những thứ đó và proxy chúng với các đối tượng IO thực, điều này sẽ hoạt động.


nếu bạn nhìn vào ctor của logger, bạn sẽ thấy rằng điều này sẽ làm rối tung quá trình xoay log. def initialize(log = nil, opt = {}) @dev = @filename = @shift_age = @shift_size = nil @mutex = LogDeviceMutex.new if log.respond_to?(:write) and log.respond_to?(:close) @dev = log else @dev = open_logfile(log) @dev.sync = true @filename = log @shift_age = opt[:shift_age] || 7 @shift_size = opt[:shift_size] || 1048576 end end
JeffCharter

3
Lưu ý trong Ruby 2.2, @targets.each(&:close)được khấu hao.
xis

Làm việc cho tôi cho đến khi tôi nhận ra rằng tôi cần phải gọi định kỳ: đóng trên log_file để lấy log_file cập nhật những gì trình ghi đã ghi (về cơ bản là "lưu"). STDOUT không thích: gần như được gọi vào nó, đại loại là đánh bại ý tưởng của MultoIO. Đã thêm một bản hack để bỏ qua: đóng ngoại trừ Tệp lớp, nhưng ước gì tôi có một giải pháp thanh lịch hơn.
Kim Miller

48

@ David's giải pháp rất tốt. Tôi đã tạo một lớp đại diện chung cho nhiều mục tiêu dựa trên mã của anh ta.

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def self.delegate(*methods)
    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end
    self
  end

  class <<self
    alias to new
  end
end

log_file = File.open("debug.log", "a")
log = Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)

Bạn có thể vui lòng giải thích, cách này là tốt hơn hoặc những gì được nâng cao tiện ích của phương pháp này so với cái đơn giản được đề xuất bởi David
Manish Sapariya

5
Đó là sự tách biệt của các mối quan tâm. MultiDelegator chỉ biết về việc ủy ​​quyền các cuộc gọi cho nhiều mục tiêu. Thực tế là một thiết bị ghi nhật ký cần một phương thức ghi và đóng được thực hiện trong trình gọi. Điều này làm cho MultiDelegator có thể sử dụng được trong các trường hợp khác ngoài ghi nhật ký.
jonas054,

Giải pháp tốt. Tôi đã cố gắng sử dụng điều này để chuyển đầu ra từ các nhiệm vụ cào của tôi thành một tệp nhật ký. Để làm cho nó hoạt động với put mặc dù (để có thể gọi $ stdout.puts mà không nhận được "phương thức riêng tư` put 'được gọi là "), tôi phải thêm một vài phương thức nữa: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: put,: print) .to (STDOUT, log_file) Sẽ rất tuyệt nếu có thể tạo một lớp Tee kế thừa từ MultiDelegator, như bạn có thể làm với các lớp người ủy nhiệm trong stdlib ...
Tyler Rick

Tôi đã nghĩ ra một cách triển khai giống như Delegator của cái này mà tôi gọi là DelegatorToAll. Bằng cách này, bạn không phải liệt kê tất cả các phương thức bạn muốn ủy quyền, vì nó sẽ ủy quyền tất cả các phương thức được định nghĩa trong lớp ủy quyền (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Xem gist.github.com/TylerRick/4990898 để biết thêm chi tiết.
Tyler Rick

1
Tôi thực sự thích giải pháp của bạn, nhưng nó không tốt khi một người ủy quyền chung có thể được sử dụng nhiều lần vì mọi ủy quyền đều gây ô nhiễm cho tất cả các trường hợp với các phương pháp mới. Tôi đã đăng một câu trả lời bên dưới ( stackoverflow.com/a/36659911/123376 ) để khắc phục sự cố này. Tôi đã đăng câu trả lời chứ không phải chỉnh sửa vì có thể mang tính giáo dục khi thấy sự khác biệt giữa hai cách triển khai vì tôi cũng đã đăng các ví dụ.
Rado

35

Nếu bạn đang sử dụng Rails 3 hoặc 4, như bài đăng trên blog này đã chỉ ra, Rails 4 được tích hợp sẵn chức năng này . Vì vậy, bạn có thể làm:

# config/environment/production.rb
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
config.logger.extend(ActiveSupport::Logger.broadcast(file_logger))

Hoặc nếu bạn đang sử dụng Rails 3, bạn có thể backport nó:

# config/initializers/alternative_output_log.rb

# backported from rails4
module ActiveSupport
  class Logger < ::Logger
    # Broadcasts logs to multiple loggers. Returns a module to be
    # `extended`'ed into other logger instances.
    def self.broadcast(logger)
      Module.new do
        define_method(:add) do |*args, &block|
          logger.add(*args, &block)
          super(*args, &block)
        end

        define_method(:<<) do |x|
          logger << x
          super(x)
        end

        define_method(:close) do
          logger.close
          super()
        end

        define_method(:progname=) do |name|
          logger.progname = name
          super(name)
        end

        define_method(:formatter=) do |formatter|
          logger.formatter = formatter
          super(formatter)
        end

        define_method(:level=) do |level|
          logger.level = level
          super(level)
        end
      end
    end
  end
end

file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(file_logger))

điều này có thể áp dụng bên ngoài đường ray, hay chỉ đường ray?
Ed Sykes

Nó dựa trên ActiveSupport, vì vậy nếu bạn đã có phụ thuộc đó, bạn có thể extendbất kỳ ActiveSupport::Loggertrường hợp nào như được hiển thị ở trên.
phillbaker

Cảm ơn, nó rất hữu ích.
Lucas

Tôi nghĩ đây là câu trả lời đơn giản và hiệu quả nhất, mặc dù tôi có một số điều kỳ lạ khi sử dụng config.logger.extend()cấu hình môi trường bên trong của mình. Thay vào đó, tôi đặt config.loggerđể STDOUTtrong môi trường của tôi, sau đó mở rộng các logger trong initializers khác nhau.
mattsch

14

Đối với những người thích nó đơn giản:

log = Logger.new("| tee test.log") # note the pipe ( '|' )
log.info "hi" # will log to both STDOUT and test.log

nguồn

Hoặc in thông báo trong trình định dạng Logger:

log = Logger.new("test.log")
log.formatter = proc do |severity, datetime, progname, msg|
    puts msg
    msg
end
log.info "hi" # will log to both STDOUT and test.log

Tôi thực sự đang sử dụng kỹ thuật này để in ra tệp nhật ký, dịch vụ ghi nhật ký đám mây (logentries) và nếu đó là môi trường nhà phát triển - cũng có thể in ra STDOUT.


2
"| tee test.log"sẽ ghi đè lên các đầu ra cũ, có thể "| tee -a test.log"thay
Fangxing

13

Trong khi tôi khá thích các đề xuất khác, tôi thấy mình gặp vấn đề tương tự nhưng muốn có khả năng có các mức ghi nhật ký khác nhau cho STDERR và tệp.

Tôi đã kết thúc với một chiến lược định tuyến ghép kênh ở cấp trình ghi thay vì ở cấp IO, để sau đó mỗi trình ghi nhật ký có thể hoạt động ở các cấp nhật ký độc lập:

class MultiLogger
  def initialize(*targets)
    @targets = targets
  end

  %w(log debug info warn error fatal unknown).each do |m|
    define_method(m) do |*args|
      @targets.map { |t| t.send(m, *args) }
    end
  end
end

stderr_log = Logger.new(STDERR)
file_log = Logger.new(File.open('logger.log', 'a'))

stderr_log.level = Logger::INFO
file_log.level = Logger::DEBUG

log = MultiLogger.new(stderr_log, file_log)

1
Tôi thích giải pháp này nhất vì nó (1) đơn giản và (2) khuyến khích bạn sử dụng lại các lớp Logger của mình thay vì giả sử mọi thứ được chuyển thành một tệp. Trong trường hợp của tôi, tôi muốn đăng nhập vào STDOUT và một ứng dụng GELF cho Graylog. Có một MultiLoggermô tả như @dsz là một sự phù hợp tuyệt vời. Cám ơn vì đã chia sẻ!
Eric Kramer

Thêm vào phần để xử lý pseudovariables (setters / getters)
Eric Kramer

11

Bạn cũng có thể thêm nhiều chức năng ghi nhật ký thiết bị trực tiếp vào Trình ghi nhật ký:

require 'logger'

class Logger
  # Creates or opens a secondary log file.
  def attach(name)
    @logdev.attach(name)
  end

  # Closes a secondary log file.
  def detach(name)
    @logdev.detach(name)
  end

  class LogDevice # :nodoc:
    attr_reader :devs

    def attach(log)
      @devs ||= {}
      @devs[log] = open_logfile(log)
    end

    def detach(log)
      @devs ||= {}
      @devs[log].close
      @devs.delete(log)
    end

    alias_method :old_write, :write
    def write(message)
      old_write(message)

      @devs ||= {}
      @devs.each do |log, dev|
        dev.write(message)
      end
    end
  end
end

Ví dụ:

logger = Logger.new(STDOUT)
logger.warn('This message goes to stdout')

logger.attach('logfile.txt')
logger.warn('This message goes both to stdout and logfile.txt')

logger.detach('logfile.txt')
logger.warn('This message goes just to stdout')

9

Đây là một cách triển khai khác, lấy cảm hứng từ @ jonas054 câu trả lời .

Điều này sử dụng một mẫu tương tự như Delegator. Bằng cách này, bạn không phải liệt kê tất cả các phương thức bạn muốn ủy quyền, vì nó sẽ ủy quyền tất cả các phương thức được xác định trong bất kỳ đối tượng đích nào:

class Tee < DelegateToAllClass(IO)
end

$stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))

Bạn cũng có thể sử dụng điều này với Logger.

Delegate_to_all.rb hiện có tại đây: https://gist.github.com/TylerRick/4990898



3

Câu trả lời của @ jonas054 ở trên rất tuyệt, nhưng nó gây ô nhiễm cho MultiDelegatorlớp với mọi đại biểu mới. Nếu bạn dùngMultiDelegator nhiều lần, nó sẽ tiếp tục thêm các phương thức vào lớp, điều này là không mong muốn. (Xem ví dụ dưới đây)

Đây là cách triển khai tương tự, nhưng sử dụng các lớp ẩn danh để các phương thức không gây ô nhiễm lớp người ủy nhiệm.

class BetterMultiDelegator

  def self.delegate(*methods)
    Class.new do
      def initialize(*targets)
        @targets = targets
      end

      methods.each do |m|
        define_method(m) do |*args|
          @targets.map { |t| t.send(m, *args) }
        end
      end

      class <<self
        alias to new
      end
    end # new class
  end # delegate

end

Đây là một ví dụ về ô nhiễm phương pháp với triển khai ban đầu, trái ngược với triển khai đã sửa đổi:

tee = MultiDelegator.delegate(:write).to(STDOUT)
tee.respond_to? :write
# => true
tee.respond_to? :size
# => false 

Tất cả là tốt ở trên. teecó một writephương pháp, nhưng không có sizephương pháp nào như mong đợi. Bây giờ, hãy xem xét khi chúng ta tạo một đại biểu khác:

tee2 = MultiDelegator.delegate(:size).to("bar")
tee2.respond_to? :size
# => true
tee2.respond_to? :write
# => true   !!!!! Bad
tee.respond_to? :size
# => true   !!!!! Bad

Ồ không, tee2phản hồi sizenhư mong đợi, nhưng nó cũng phản hồi writevì đại biểu đầu tiên. Ngay cả teebây giờ cũng phản hồisize vì ô nhiễm phương pháp.

Đối chiếu điều này với giải pháp lớp ẩn danh, mọi thứ đều như mong đợi:

see = BetterMultiDelegator.delegate(:write).to(STDOUT)
see.respond_to? :write
# => true
see.respond_to? :size
# => false

see2 = BetterMultiDelegator.delegate(:size).to("bar")
see2.respond_to? :size
# => true
see2.respond_to? :write
# => false
see.respond_to? :size
# => false

2

Bạn có bị giới hạn trong trình ghi nhật ký tiêu chuẩn không?

Nếu không, bạn có thể sử dụng log4r :

require 'log4r' 

LOGGER = Log4r::Logger.new('mylog')
LOGGER.outputters << Log4r::StdoutOutputter.new('stdout')
LOGGER.outputters << Log4r::FileOutputter.new('file', :filename => 'test.log') #attach to existing log-file

LOGGER.info('aa') #Writs on STDOUT and sends to file

Một ưu điểm: Bạn cũng có thể xác định các cấp độ nhật ký khác nhau cho stdout và tệp.


1

Tôi đã đi đến cùng một ý tưởng "Ủy quyền tất cả các phương thức cho các phần tử con" mà những người khác đã khám phá, nhưng trả về cho mỗi phương thức đó giá trị trả về của lần gọi cuối cùng của phương thức. Nếu tôi không làm như vậy, nó đã phá vỡ logger-colorsđiều đang mong đợi một Integervà bản đồ đang trả về một Array.

class MultiIO
  def self.delegate_all
    IO.methods.each do |m|
      define_method(m) do |*args|
        ret = nil
        @targets.each { |t| ret = t.send(m, *args) }
        ret
      end
    end
  end

  def initialize(*targets)
    @targets = targets
    MultiIO.delegate_all
  end
end

Điều này sẽ chuyển hướng mọi phương thức đến tất cả các mục tiêu và chỉ trả về giá trị trả về của lần gọi cuối cùng.

Ngoài ra, nếu bạn muốn có màu sắc, STDOUT hoặc STDERR phải được đặt cuối cùng, vì đó là hai màu duy nhất được cho là xuất ra. Nhưng sau đó, nó cũng sẽ xuất màu vào tệp của bạn.

logger = Logger.new MultiIO.new(File.open("log/test.log", 'w'), STDOUT)
logger.error "Roses are red"
logger.unknown "Violets are blue"

1

Tôi đã viết một chút RubyGem cho phép bạn thực hiện một số điều sau:

# Pipe calls to an instance of Ruby's logger class to $stdout
require 'teerb'

log_file = File.open("debug.log", "a")
logger = Logger.new(TeeRb::IODelegate.new(log_file, STDOUT))

logger.warn "warn"
$stderr.puts "stderr hello"
puts "stdout hello"

Bạn có thể tìm thấy mã trên github: teerb


1

Một cách nữa. Nếu bạn đang sử dụng ghi nhật ký được gắn thẻ và cũng cần các thẻ trong một tệp nhật ký khác, bạn có thể thực hiện theo cách này

# backported from rails4
# config/initializers/active_support_logger.rb
module ActiveSupport
 class Logger < ::Logger

 # Broadcasts logs to multiple loggers. Returns a module to be
 # `extended`'ed into other logger instances.
 def self.broadcast(logger)
  Module.new do
    define_method(:add) do |*args, &block|
      logger.add(*args, &block)
      super(*args, &block)
    end

    define_method(:<<) do |x|
      logger << x
      super(x)
    end

    define_method(:close) do
      logger.close
      super()
    end

    define_method(:progname=) do |name|
      logger.progname = name
      super(name)
    end

    define_method(:formatter=) do |formatter|
      logger.formatter = formatter
      super(formatter)
    end

    define_method(:level=) do |level|
      logger.level = level
      super(level)
    end

   end # Module.new
 end # broadcast

 def initialize(*args)
   super
   @formatter = SimpleFormatter.new
 end

  # Simple formatter which only displays the message.
  class SimpleFormatter < ::Logger::Formatter
   # This method is invoked when a log event occurs
   def call(severity, time, progname, msg)
   element = caller[4] ? caller[4].split("/").last : "UNDEFINED"
    "#{Thread.current[:activesupport_tagged_logging_tags]||nil } # {time.to_s(:db)} #{severity} #{element} -- #{String === msg ? msg : msg.inspect}\n"
   end
  end

 end # class Logger
end # module ActiveSupport

custom_logger = ActiveSupport::Logger.new(Rails.root.join("log/alternative_#{Rails.env}.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(custom_logger))

Sau đó, bạn sẽ nhận được các thẻ uuid trong trình ghi thay thế

["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO logger.rb:28:in `call_app' -- 
["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO   logger.rb:31:in `call_app' -- Started POST "/psp/entrypoint" for 192.168.56.1 at 2015-03-12 16:54:04 +0700

Hy vọng rằng sẽ giúp một ai đó.


Đơn giản, đáng tin cậy và hoạt động tuyệt vời. Cảm ơn! Lưu ý rằng ActiveSupport::Loggerhoạt động ngoài hộp với điều này - bạn chỉ cần sử dụng Rails.logger.extendvới ActiveSupport::Logger.broadcast(...).
XtraSimpionaire

0

Một lựa chọn nữa ;-)

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def method_missing(method_sym, *arguments, &block)
    @targets.each do |target|
      target.send(method_sym, *arguments, &block) if target.respond_to?(method_sym)
    end
  end
end

log = MultiDelegator.new(Logger.new(STDOUT), Logger.new(File.open("debug.log", "a")))

log.info('Hello ...')

0

Tôi thích cách tiếp cận MultiIO . Nó hoạt động tốt với Ruby Logger . Nếu bạn sử dụng IO thuần túy, nó sẽ ngừng hoạt động vì nó thiếu một số phương thức mà các đối tượng IO được mong đợi có. Pipes đã được đề cập trước đây ở đây: Làm cách nào để có thể xuất bản ghi nhật ký ruby ​​logger thành stdout cũng như tệp? . Đây là những gì phù hợp nhất với tôi.

def watch(cmd)
  output = StringIO.new
  IO.popen(cmd) do |fd|
    until fd.eof?
      bit = fd.getc
      output << bit
      $stdout.putc bit
    end
  end
  output.rewind
  [output.read, $?.success?]
ensure
  output.close
end

result, success = watch('./my/shell_command as a String')

Lưu ý Tôi biết điều này không trả lời câu hỏi trực tiếp nhưng nó có liên quan chặt chẽ. Bất cứ khi nào tôi tìm kiếm đầu ra cho nhiều IO, tôi đều bắt gặp chuỗi này. Vì vậy, tôi hy vọng bạn cũng thấy điều này hữu ích.


0

Đây là sự đơn giản hóa giải pháp của @ rado.

def delegator(*methods)
  Class.new do
    def initialize(*targets)
      @targets = targets
    end

    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end

    class << self
      alias for new
    end
  end # new class
end # delegate

Nó có tất cả các lợi ích giống như của mình mà không cần đến lớp bao bọc bên ngoài. Nó là một tiện ích hữu ích để có trong một tệp ruby ​​riêng biệt.

Sử dụng nó như một lớp lót để tạo các phiên bản ủy quyền như sau:

IO_delegator_instance = delegator(:write, :read).for(STDOUT, STDERR)
IO_delegator_instance.write("blah")

HOẶC sử dụng nó như một nhà máy như vậy:

logger_delegator_class = delegator(:log, :warn, :error)
secret_delegator = logger_delegator_class(main_logger, secret_logger)
secret_delegator.warn("secret")

general_delegator = logger_delegator_class(main_logger, debug_logger, other_logger) 
general_delegator.log("message")

0

Bạn có thể sử dụng Loog::Teeđối tượng từ loogđá quý:

require 'loog'
logger = Loog::Tee.new(first, second)

Chính xác những gì bạn đang tìm kiếm.


0

Nếu bạn thấy ổn với việc sử dụng ActiveSupport, thì tôi thực sự khuyên bạn nên kiểm traActiveSupport::Logger.broadcast , đây là một cách tuyệt vời và rất ngắn gọn để thêm các điểm đến nhật ký bổ sung vào trình ghi nhật ký.

Trên thực tế, nếu bạn đang sử dụng Rails 4+ (kể từ bản cam kết này ), bạn không cần phải làm bất cứ điều gì để có được hành vi mong muốn - ít nhất là nếu bạn đang sử dụng rails console. Bất cứ khi nào bạn sử dụng rails console, Rails sẽ tự động mở rộng Rails.loggerđể nó xuất cả đến đích tệp thông thường ( log/production.logví dụ:) và STDERR:

    console do |app|
      
      unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
        console = ActiveSupport::Logger.new(STDERR)
        Rails.logger.extend ActiveSupport::Logger.broadcast console
      end
      ActiveRecord::Base.verbose_query_logs = false
    end

Vì một số lý do không xác định và không may, phương pháp này không có tài liệu nhưng bạn có thể tham khảo mã nguồn hoặc các bài đăng trên blog để tìm hiểu cách hoạt động hoặc xem các ví dụ.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destination-using-activesupport-4.html có một ví dụ khác:

require "active_support/logger"
console_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("my_log.log")
combined_logger = console_logger.extend(ActiveSupport::Logger.broadcast(file_logger))

combined_logger.debug "Debug level"

0

Tôi cũng có nhu cầu này gần đây vì vậy tôi đã triển khai một thư viện thực hiện điều này. Tôi vừa phát hiện ra câu hỏi StackOverflow này, vì vậy tôi sẽ đưa nó ra cho bất kỳ ai cần nó: https://github.com/agis/multi_io .

So với các giải pháp khác được đề cập ở đây, điều này cố gắng trở thành một IOđối tượng của riêng nó, vì vậy nó có thể được sử dụng như một sự thay thế thả vào cho các đối tượng IO thông thường khác (tệp, ổ cắm, v.v.)

Điều đó nói rằng, tôi vẫn chưa triển khai tất cả các phương thức IO tiêu chuẩn, nhưng những phương thức đó tuân theo ngữ nghĩa IO (ví dụ: #writetrả về tổng số byte được ghi cho tất cả các mục tiêu IO cơ bản).


-3

Tôi nghĩ rằng STDOUT của bạn được sử dụng cho thông tin thời gian chạy quan trọng và các lỗi phát sinh.

Vì vậy, tôi sử dụng

  $log = Logger.new('process.log', 'daily')

để ghi nhật ký gỡ lỗi và ghi nhật ký thông thường, sau đó viết một vài

  puts "doing stuff..."

nơi tôi cần xem thông tin STDOUT rằng các tập lệnh của tôi đang chạy!

Bah, chỉ 10 xu của tôi :-)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.