Làm cách nào để bật / tắt cấp độ nhật ký trong Android?


149

Tôi đang có rất nhiều báo cáo đăng nhập để gỡ lỗi chẳng hạn.

Log.v(TAG, "Message here");
Log.w(TAG, " WARNING HERE");

Trong khi triển khai ứng dụng này trên điện thoại thiết bị, tôi muốn tắt ghi nhật ký chi tiết từ nơi tôi có thể bật / tắt ghi nhật ký.


Bản sao có thể trùng lặp của các cấp ghi nhật ký Android

Câu trả lời:


80

Một cách phổ biến là tạo một int có tên loglevel và xác định mức độ gỡ lỗi của nó dựa trên loglevel.

public static int LOGLEVEL = 2;
public static boolean ERROR = LOGLEVEL > 0;
public static boolean WARN = LOGLEVEL > 1;
...
public static boolean VERBOSE = LOGLEVEL > 4;

    if (VERBOSE) Log.v(TAG, "Message here"); // Won't be shown
    if (WARN) Log.w(TAG, "WARNING HERE");    // Still goes through

Sau đó, bạn chỉ có thể thay đổi LOGLEVEL cho tất cả các mức đầu ra gỡ lỗi.


1
tốt, nhưng bạn sẽ vô hiệu hóa DEBUG trong ví dụ của mình như thế nào, nhưng vẫn hiển thị cảnh báo ....
Andre Bossard

1
Các câu lệnh if có kết thúc bằng mã byte .apk không? Tôi nghĩ rằng chúng tôi muốn (nói chung) tắt đăng nhập khi ứng dụng được triển khai nhưng câu lệnh if sẽ không bị xóa.
cờ vua

2
trong ví dụ của bạn, các thông báo DEBUG sẽ được hiển thị, trong khi WARN sẽ không? bình thường bạn có muốn đối diện không?
Sam

15
Sử dụng BuildConfig.DEBUG thay vì các biến tùy chỉnh
hB0

1
@chessofnerd "trong Java, mã bên trong if thậm chí sẽ không phải là một phần của mã được biên dịch. Nó phải được biên dịch, nhưng nó sẽ không được ghi vào mã byte được biên dịch." stackoverflow.com/questions/7122723/
Mạnh

197

Các tài liệu Android nói như sau về Levels Log :

Verbose không bao giờ nên được biên dịch thành một ứng dụng trừ khi phát triển. Nhật ký gỡ lỗi được biên dịch trong nhưng bị tước khi chạy. Nhật ký lỗi, cảnh báo và thông tin luôn được lưu giữ.

Vì vậy, bạn có thể muốn xem xét tước bỏ các báo cáo ghi nhật ký Verbose, có thể sử dụng ProGuard như được đề xuất trong câu trả lời khác .

Theo tài liệu, bạn có thể định cấu hình ghi nhật ký trên thiết bị phát triển bằng System Properties. Các tài sản để thiết lập là log.tag.<YourTag>và nó nên được đặt thành một trong những giá trị sau: VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, hoặc SUPPRESS. Thông tin thêm về điều này có sẵn trong tài liệu cho isLoggable()phương pháp.

Bạn có thể đặt thuộc tính tạm thời bằng cách sử dụng setproplệnh. Ví dụ:

C:\android>adb shell setprop log.tag.MyAppTag WARN
C:\android>adb shell getprop log.tag.MyAppTag
WARN

Ngoài ra, bạn có thể chỉ định chúng trong tệp '/data/local.prop' như sau:

log.tag.MyAppTag=WARN

Các phiên bản Android mới hơn dường như chỉ yêu cầu /data/local.prop chỉ được đọc . Tập tin này được đọc khi khởi động, vì vậy bạn sẽ cần phải khởi động lại sau khi cập nhật nó. Nếu /data/local.propthế giới có thể ghi, nó có thể sẽ bị bỏ qua.

Cuối cùng, bạn có thể thiết lập chúng theo chương trình bằng System.setProperty()phương pháp .


4
Tôi đã có cùng trải nghiệm; các tài liệu API khá không rõ ràng về cách thức hoạt động của nó và thậm chí dường như đề cập đến hầu hết các android.util.Confighằng số bị phản đối. Các giá trị được mã hóa cứng được chỉ định trong tài liệu API là vô ích vì các giá trị này (được cho là) ​​thay đổi theo bản dựng. Do đó, tuyến đường ProGuard dường như là giải pháp tốt nhất cho chúng tôi.
Christopher Orr

3
Bạn đã có may mắn trong việc định cấu hình ghi nhật ký Android bằng tệp /data/local.prop, phương thức setprop hoặc với System.setProperty chưa? Tôi gặp khá nhiều rắc rối khi Log.isLoggable (TAG, ĐỘNG TỪ) trở lại đúng với tôi.
seanoshea

2
Tôi đã nhận được gỡ lỗi Android làm việc. Mẹo nhỏ là khi bạn gọi một cái gì đó như Log.d ("xyz"), thông báo được ghi vào logcat ngay cả khi gỡ lỗi bị vô hiệu hóa cho logger. Điều này có nghĩa là lọc thường xảy ra sau khi được viết. Để lọc trước một cái gì đó như Log.isLoggable (TAG, Log.VERBOSE)) {Log.v (TAG, "thông điệp nhật ký của tôi"); } là cần thiết. Điều này nói chung là khá mệt mỏi. Tôi sử dụng một phiên bản sửa đổi của slf4j-android để có được những gì tôi muốn.
Phreed

2
@Dave bạn đã bao giờ có thể làm cho phương thức local.prop hoạt động chính xác. Tôi cũng không thể thực hiện công việc này, tôi đã tạo một mục log.tag.test = INFO và sau đó cố gắng thay đổi nó chạy setprop log.tag.test SUPPRESS từ vỏ adb và nó không thay đổi gì cả. Ngoài ra, sử dụng System.getProperty và System.setProperty không có gì. Muốn nhận được một bản cập nhật từ bạn. Cảm ơn.
jjNford

2
+1 cho nhận xét "tài liệu API khá không rõ ràng về cách chính xác cách thức hoạt động của nó".
Alan

90

Cách dễ nhất có lẽ là chạy JAR đã biên dịch của bạn thông qua ProGuard trước khi triển khai, với cấu hình như:

-assumenosideeffects class android.util.Log {
    public static int v(...);
}

Điều đó sẽ - ngoài tất cả các tối ưu hóa ProGuard khác - loại bỏ bất kỳ câu lệnh nhật ký dài dòng nào trực tiếp khỏi mã byte.


làm nó có chứa bất kỳ tệp log.property nào trong đó chúng ta có thể xác định cài đặt.
d-man

1
tước bỏ các dòng với proguard có nghĩa là dấu vết ngăn xếp của bạn từ sản xuất có thể không khớp với mã của bạn.
larham1

3
@ larham1: ProGuard hoạt động trên mã byte, vì vậy tôi sẽ tưởng tượng việc xóa các cuộc gọi đăng nhập sẽ không làm thay đổi siêu dữ liệu số dòng được nhúng.
Christopher Orr

19
Lưu ý điều này - ngay cả khi cuộc gọi thực tế đến Log.v () đang bị tước, các đối số của nó vẫn được đánh giá. Vì vậy, nếu bạn có một số cuộc gọi phương thức tốn kém bên trong, ví dụ Log.v (TAG, createdLog ()), nó có thể ảnh hưởng đến hiệu suất của bạn nếu trong một số đường dẫn mã nóng. Ngay cả những thứ như toString () hoặc String.format () cũng có thể có ý nghĩa.
Błażej Czapp

4
@GaneshKrishnan Không, điều đó không đúng. Cuộc gọi đến Log.v () bị tước nhưng theo mặc định, các lệnh gọi phương thức để xây dựng chuỗi sẽ không bị xóa. Xem câu trả lời này từ tác giả của ProGuard: stackoverflow.com/a/6023505/234938
Christopher Orr

18

Tôi đã thực hiện một lộ trình đơn giản - tạo một lớp bao bọc cũng sử dụng danh sách tham số biến.

 public class Log{
        public static int LEVEL = android.util.Log.WARN;


    static public void d(String tag, String msgFormat, Object...args)
    {
        if (LEVEL<=android.util.Log.DEBUG)
        {
            android.util.Log.d(tag, String.format(msgFormat, args));
        }
    }

    static public void d(String tag, Throwable t, String msgFormat, Object...args)
    {
        if (LEVEL<=android.util.Log.DEBUG)
        {
            android.util.Log.d(tag, String.format(msgFormat, args), t);
        }
    }

    //...other level logging functions snipped

1
Như tôi đã đề cập ở trên. Tôi đã sử dụng một phiên bản sửa đổi của slf4j-android để thực hiện kỹ thuật này.
Phreed

3
Có một mối quan tâm lớn về điều đó, hãy xem stackoverflow.com/questions/2446248/ trên
OneWorld

10

Cách tốt hơn là sử dụng API SLF4J + một số cách triển khai.

Đối với các ứng dụng Android, bạn có thể sử dụng như sau:

  1. Android Logger là triển khai SLF4J nhẹ nhưng dễ cấu hình (<50 Kb).
  2. LOGBack là triển khai mạnh mẽ và được tối ưu hóa nhất nhưng kích thước của nó là khoảng 1 Mb.
  3. Bất kỳ khác theo sở thích của bạn: slf4j-android, slf4android.

2
Trên Android, bạn sẽ phải sử dụng logback-android(vì logbackthích hợp là không tương thích). logback-android-1.0.10-1.jarlà 429 KB, không quá tệ khi xem xét các tính năng được cung cấp, nhưng hầu hết các nhà phát triển sẽ sử dụng Proguard để tối ưu hóa ứng dụng của họ.
tony19

Điều này không lưu u từ việc sử dụng các câu lệnh if để kiểm tra mức độ nhật ký trước khi đăng nhập. Xem stackoverflow.com/questions/4958860/
Mạnh

8

Bạn nên sử dụng

    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "my log message");
    }

2
Làm thế nào để cấu hình đầu ra của isLoggable? Có phải gỡ lỗi và verbose không thể loggable khi isDebugable được đặt sai trong tệp kê khai không?
OneWorld

5

Tước đăng nhập bằng proguard (xem câu trả lời từ @Christopher) rất dễ dàng và nhanh chóng, nhưng nó đã gây ra dấu vết ngăn xếp từ sản xuất để không khớp với nguồn nếu có bất kỳ ghi nhật ký gỡ lỗi nào trong tệp.

Thay vào đó, đây là một kỹ thuật sử dụng các mức ghi nhật ký khác nhau trong phát triển so với sản xuất, giả sử rằng proguard chỉ được sử dụng trong sản xuất. Nó nhận ra việc sản xuất bằng cách xem liệu proguard có đổi tên một tên lớp nhất định hay không (trong ví dụ này, tôi sử dụng "com.foo.Bar" - bạn sẽ thay thế tên này bằng một tên lớp đủ điều kiện mà bạn biết sẽ được đổi tên bởi proguard).

Kỹ thuật này sử dụng đăng nhập commons.

private void initLogging() {
    Level level = Level.WARNING;
    try {
        // in production, the shrinker/obfuscator proguard will change the
        // name of this class (and many others) so in development, this
        // class WILL exist as named, and we will have debug level
        Class.forName("com.foo.Bar");
        level = Level.FINE;
    } catch (Throwable t) {
        // no problem, we are in production mode
    }
    Handler[] handlers = Logger.getLogger("").getHandlers();
    for (Handler handler : handlers) {
        Log.d("log init", "handler: " + handler.getClass().getName());
        handler.setLevel(level);
    }
}


3

Có một sự thay thế nhỏ trong lớp Nhật ký Android tiêu chuẩn - https://github.com/zserge/log

Về cơ bản tất cả các bạn phải làm là để thay thế hàng nhập khẩu từ android.util.Logđến trikita.log.Log. Sau đó, trong Application.onCreate()hoặc trong một số initalizer tĩnh, hãy kiểm tra BuilConfig.DEBUGhoặc bất kỳ cờ nào khác và sử dụng Log.level(Log.D)hoặc Log.level(Log.E)để thay đổi mức ghi nhật ký tối thiểu. Bạn có thể sử dụng Log.useLog(false)để vô hiệu hóa đăng nhập cả.


2

Có thể bạn có thể thấy lớp mở rộng Nhật ký này: https://github.com/dbauduin/Android-Tools/tree/master/logs .

Nó cho phép bạn kiểm soát tốt các bản ghi. Ví dụ, bạn có thể vô hiệu hóa tất cả các bản ghi hoặc chỉ nhật ký của một số gói hoặc lớp.

Hơn nữa, nó bổ sung một số chức năng hữu ích (ví dụ: bạn không phải truyền thẻ cho mỗi nhật ký).


2

Tôi đã tạo một Utility / Wrapper để giải quyết vấn đề này + các vấn đề phổ biến khác xung quanh Logging.

Một tiện ích gỡ lỗi với các tính năng sau:

  • Các tính năng thông thường được cung cấp bởi lớp Log được bao bọc bởi LogMode s.
  • Nhật ký xuất nhập phương thức: Có thể tắt bằng công tắc
  • Gỡ lỗi chọn lọc: Gỡ lỗi các lớp cụ thể.
  • Phương pháp thực hiện - Đo thời gian: Đo thời gian thực hiện cho các phương thức riêng lẻ cũng như thời gian tập thể dành cho tất cả các phương thức của một lớp.

Sử dụng như thế nào?

  • Bao gồm các lớp trong dự án của bạn.
  • Sử dụng nó giống như bạn sử dụng các phương thức android.util.Log, để bắt đầu.
  • Sử dụng tính năng nhật ký Entry-Exit bằng cách đặt các lệnh gọi đến các phương thức entry_log () - exit_log () ở đầu và cuối của các phương thức trong ứng dụng của bạn.

Tôi đã cố gắng để làm cho các tài liệu tự mãn.

Đề xuất cải thiện Tiện ích này được chào đón.

Miễn phí sử dụng / chia sẻ.

Tải xuống từ GitHub .


2

Đây là một giải pháp phức tạp hơn. Bạn sẽ nhận được theo dõi ngăn xếp đầy đủ và phương thức toString () sẽ chỉ được gọi nếu cần (Hiệu suất). Thuộc tính BuildConfig.DEBUG sẽ sai trong chế độ sản xuất, vì vậy tất cả các nhật ký theo dõi và gỡ lỗi sẽ bị xóa. Trình biên dịch điểm nóng có cơ hội loại bỏ các cuộc gọi vì tắt các thuộc tính tĩnh cuối cùng.

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import android.util.Log;

public class Logger {

    public enum Level {
        error, warn, info, debug, trace
    }

    private static final String DEFAULT_TAG = "Project";

    private static final Level CURRENT_LEVEL = BuildConfig.DEBUG ? Level.trace : Level.info;

    private static boolean isEnabled(Level l) {
        return CURRENT_LEVEL.compareTo(l) >= 0;
    }

    static {
        Log.i(DEFAULT_TAG, "log level: " + CURRENT_LEVEL.name());
    }

    private String classname = DEFAULT_TAG;

    public void setClassName(Class<?> c) {
        classname = c.getSimpleName();
    }

    public String getClassname() {
        return classname;
    }

    public boolean isError() {
        return isEnabled(Level.error);
    }

    public boolean isWarn() {
        return isEnabled(Level.warn);
    }

    public boolean isInfo() {
        return isEnabled(Level.info);
    }

    public boolean isDebug() {
        return isEnabled(Level.debug);
    }

    public boolean isTrace() {
        return isEnabled(Level.trace);
    }

    public void error(Object... args) {
        if (isError()) Log.e(buildTag(), build(args));
    }

    public void warn(Object... args) {
        if (isWarn()) Log.w(buildTag(), build(args));
    }

    public void info(Object... args) {
        if (isInfo()) Log.i(buildTag(), build(args));
    }

    public void debug(Object... args) {
        if (isDebug()) Log.d(buildTag(), build(args));
    }

    public void trace(Object... args) {
        if (isTrace()) Log.v(buildTag(), build(args));
    }

    public void error(String msg, Throwable t) {
        if (isError()) error(buildTag(), msg, stackToString(t));
    }

    public void warn(String msg, Throwable t) {
        if (isWarn()) warn(buildTag(), msg, stackToString(t));
    }

    public void info(String msg, Throwable t) {
        if (isInfo()) info(buildTag(), msg, stackToString(t));
    }

    public void debug(String msg, Throwable t) {
        if (isDebug()) debug(buildTag(), msg, stackToString(t));
    }

    public void trace(String msg, Throwable t) {
        if (isTrace()) trace(buildTag(), msg, stackToString(t));
    }

    private String buildTag() {
        String tag ;
        if (BuildConfig.DEBUG) {
            StringBuilder b = new StringBuilder(20);
            b.append(getClassname());

            StackTraceElement stackEntry = Thread.currentThread().getStackTrace()[4];
            if (stackEntry != null) {
                b.append('.');
                b.append(stackEntry.getMethodName());
                b.append(':');
                b.append(stackEntry.getLineNumber());
            }
            tag = b.toString();
        } else {
            tag = DEFAULT_TAG;
        }
    }

    private String build(Object... args) {
        if (args == null) {
            return "null";
        } else {
            StringBuilder b = new StringBuilder(args.length * 10);
            for (Object arg : args) {
                if (arg == null) {
                    b.append("null");
                } else {
                    b.append(arg);
                }
            }
            return b.toString();
        }
    }

    private String stackToString(Throwable t) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(500);
        baos.toString();
        t.printStackTrace(new PrintStream(baos));
        return baos.toString();
    }
}

sử dụng như thế này:

Loggor log = new Logger();
Map foo = ...
List bar = ...
log.error("Foo:", foo, "bar:", bar);
// bad example (avoid something like this)
// log.error("Foo:" + " foo.toString() + "bar:" + bar); 

1

Trong một kịch bản ghi nhật ký rất đơn giản, trong đó bạn thực sự chỉ cố gắng ghi vào bảng điều khiển trong quá trình phát triển cho mục đích gỡ lỗi, có thể dễ dàng nhất là thực hiện tìm kiếm và thay thế trước khi sản xuất của bạn xây dựng và nhận xét tất cả các lệnh gọi tới Nhật ký hoặc Hệ thống. ra.println.

Ví dụ: giả sử bạn không sử dụng "Nhật ký". bất cứ nơi nào bên ngoài một cuộc gọi đến Log.d hoặc Log.e, v.v., bạn chỉ cần tìm và thay thế toàn bộ giải pháp để thay thế "Nhật ký". với "// Đăng nhập." để nhận xét tất cả các cuộc gọi đăng nhập của bạn hoặc trong trường hợp của tôi, tôi chỉ sử dụng System.out.println ở mọi nơi, vì vậy trước khi đi vào sản xuất, tôi chỉ cần thực hiện tìm kiếm đầy đủ và thay thế cho "System.out.println" và thay thế bằng "//System.out.println".

Tôi biết điều này không lý tưởng và sẽ rất tuyệt nếu khả năng tìm và nhận xét các cuộc gọi tới Log và System.out.println được tích hợp vào Eclipse, nhưng cho đến khi điều đó xảy ra cách dễ nhất và nhanh nhất và tốt nhất để làm điều này là để bình luận bằng cách tìm kiếm và thay thế. Nếu bạn làm điều này, bạn không phải lo lắng về việc không khớp các số dòng theo dõi ngăn xếp, bởi vì bạn đang chỉnh sửa mã nguồn của mình và bạn không thêm bất kỳ chi phí nào bằng cách kiểm tra một số cấu hình cấp độ nhật ký, v.v.


1

Trong các ứng dụng của mình, tôi có một lớp bao bọc lớp Log có var boolean tĩnh được gọi là "state". Trong suốt mã của tôi, tôi kiểm tra giá trị của biến "trạng thái" bằng phương thức tĩnh trước khi thực sự ghi vào Nhật ký. Sau đó, tôi có một phương thức tĩnh để đặt biến "trạng thái" để đảm bảo giá trị là phổ biến trên tất cả các phiên bản được tạo bởi ứng dụng. Điều này có nghĩa là tôi có thể bật hoặc tắt tất cả ghi nhật ký cho Ứng dụng trong một cuộc gọi - ngay cả khi Ứng dụng đang chạy. Hữu ích cho các cuộc gọi hỗ trợ ... Điều đó có nghĩa là bạn phải bám vào súng khi gỡ lỗi và không thụt lùi khi sử dụng lớp Log tiêu chuẩn mặc dù ...

Nó cũng hữu ích (thuận tiện) khi Java diễn giải var boolean là false nếu nó chưa được gán một giá trị, điều đó có nghĩa là nó có thể bị sai thành sai cho đến khi bạn cần bật ghi nhật ký :-)


1

Chúng ta có thể sử dụng lớp Logtrong thành phần cục bộ của mình và định nghĩa các phương thức là v / i / e / d. Dựa trên nhu cầu của chúng tôi có thể thực hiện cuộc gọi hơn nữa.
ví dụ được hiển thị dưới đây.

    public class Log{
        private static boolean TAG = false;
        public static void d(String enable_tag, String message,Object...args){
            if(TAG)
            android.util.Log.d(enable_tag, message+args);
        }
        public static void e(String enable_tag, String message,Object...args){
            if(TAG)
            android.util.Log.e(enable_tag, message+args);
        }
        public static void v(String enable_tag, String message,Object...args){
            if(TAG)
            android.util.Log.v(enable_tag, message+args);
        }
    }
    if we do not need any print(s), at-all make TAG as false for all else 
    remove the check for type of Log (say Log.d).
    as 
    public static void i(String enable_tag, String message,Object...args){
    //      if(TAG)
            android.util.Log.i(enable_tag, message+args);
    }

ở đây tin nhắn là cho stringargslà giá trị bạn muốn in.


0

Đối với tôi thường rất hữu ích khi có thể đặt các mức nhật ký khác nhau cho mỗi TAG.

Tôi đang sử dụng lớp trình bao bọc rất đơn giản này:

public class Log2 {

    public enum LogLevels {
        VERBOSE(android.util.Log.VERBOSE), DEBUG(android.util.Log.DEBUG), INFO(android.util.Log.INFO), WARN(
                android.util.Log.WARN), ERROR(android.util.Log.ERROR);

        int level;

        private LogLevels(int logLevel) {
            level = logLevel;
        }

        public int getLevel() {
            return level;
        }
    };

    static private HashMap<String, Integer> logLevels = new HashMap<String, Integer>();

    public static void setLogLevel(String tag, LogLevels level) {
        logLevels.put(tag, level.getLevel());
    }

    public static int v(String tag, String msg) {
        return Log2.v(tag, msg, null);
    }

    public static int v(String tag, String msg, Throwable tr) {
        if (logLevels.containsKey(tag)) {
            if (logLevels.get(tag) > android.util.Log.VERBOSE) {
                return -1;
            }
        }
        return Log.v(tag, msg, tr);
    }

    public static int d(String tag, String msg) {
        return Log2.d(tag, msg, null);
    }

    public static int d(String tag, String msg, Throwable tr) {
        if (logLevels.containsKey(tag)) {
            if (logLevels.get(tag) > android.util.Log.DEBUG) {
                return -1;
            }
        }
        return Log.d(tag, msg);
    }

    public static int i(String tag, String msg) {
        return Log2.i(tag, msg, null);
    }

    public static int i(String tag, String msg, Throwable tr) {
        if (logLevels.containsKey(tag)) {
            if (logLevels.get(tag) > android.util.Log.INFO) {
                return -1;
            }
        }
        return Log.i(tag, msg);
    }

    public static int w(String tag, String msg) {
        return Log2.w(tag, msg, null);
    }

    public static int w(String tag, String msg, Throwable tr) {
        if (logLevels.containsKey(tag)) {
            if (logLevels.get(tag) > android.util.Log.WARN) {
                return -1;
            }
        }
        return Log.w(tag, msg, tr);
    }

    public static int e(String tag, String msg) {
        return Log2.e(tag, msg, null);
    }

    public static int e(String tag, String msg, Throwable tr) {
        if (logLevels.containsKey(tag)) {
            if (logLevels.get(tag) > android.util.Log.ERROR) {
                return -1;
            }
        }
        return Log.e(tag, msg, tr);
    }

}

Bây giờ chỉ cần đặt mức ghi nhật ký cho mỗi TAG ở đầu mỗi lớp:

Log2.setLogLevel(TAG, LogLevels.INFO);

0

Một cách khác là sử dụng một nền tảng ghi nhật ký có khả năng mở và đóng nhật ký. Điều này đôi khi có thể mang lại sự linh hoạt ngay cả trên một ứng dụng sản xuất, các bản ghi nên được mở và đóng tùy thuộc vào vấn đề bạn có ví dụ:

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.